source-code-mgmt 1.1.0 → 1.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.
- package/README.md +144 -64
- package/lib/client.js +897 -122
- package/lib/index.js +643 -59
- package/package.json +38 -22
package/lib/client.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* source-code-mgmt — browser half (client.js).
|
|
3
3
|
*
|
|
4
|
-
* Classic-script plugin bundle (no build step)
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Classic-script plugin bundle (no build step). On activation it detects
|
|
5
|
+
* whether dsh-better-sidebar is installed (a single in-memory `ctx.get`,
|
|
6
|
+
* zero I/O — does not slow DSH startup):
|
|
7
|
+
*
|
|
8
|
+
* - branch A: dsh-better-sidebar installed → register「代码管理」as a new
|
|
9
|
+
* sidebar Tab page through `ctx.betterSidebar.registerTab`.
|
|
10
|
+
* - branch B: not installed → render a floating button pinned to the top
|
|
11
|
+
* right; clicking expands a right-side drawer (like better-sidebar's
|
|
12
|
+
* right rail) holding the existing panel.
|
|
13
|
+
*
|
|
14
|
+
* The old left-rail bottom button (`sidebar.footer.action` slot) is removed.
|
|
15
|
+
* Either way clicking the entry opens a "源代码管理" panel with:
|
|
8
16
|
*
|
|
9
17
|
* 1. 环境检查 — git / gh presence & version
|
|
10
18
|
* 2. SSH — key presence, ed25519 generation, github.com ssh config, test
|
|
@@ -70,12 +78,50 @@ window.__ModuleLoader__.load({
|
|
|
70
78
|
ssh: null,
|
|
71
79
|
defDir: null,
|
|
72
80
|
repo: null,
|
|
81
|
+
// 各工作区的 full 同步结果(dir -> repo 状态),打开 DSH 时预取,切换工作区可秒显。
|
|
82
|
+
reposByDir: {},
|
|
83
|
+
// 单文件改动 diff 缓存("dir\u0000path" -> diff 文本),点击「查看」秒出。
|
|
84
|
+
diffs: {},
|
|
73
85
|
workspaces: [],
|
|
74
86
|
customDirs: [],
|
|
75
87
|
ready: false,
|
|
76
88
|
};
|
|
77
89
|
let preloadPromise = null;
|
|
78
90
|
|
|
91
|
+
/** 清空某个目录(或全部)的 diff 缓存,避免切目录后残留上一个目录的文件 diff。 */
|
|
92
|
+
function clearDiffCache(dir) {
|
|
93
|
+
if (!dir) { cache.diffs = {}; return }
|
|
94
|
+
const prefix = dir + "\u0000"
|
|
95
|
+
for (const key of Object.keys(cache.diffs)) {
|
|
96
|
+
if (key.startsWith(prefix)) delete cache.diffs[key]
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 并发上限 N 地预取一批改动文件的 diff,结果写入 cache.diffs,用于点击「查看」秒出。
|
|
101
|
+
* 纯本地 git 读取(host 端 /repo-diff),并发低、不阻塞主流程,后台慢慢填。 */
|
|
102
|
+
async function prefetchDiffs(dir, files, limit) {
|
|
103
|
+
if (!dir || !Array.isArray(files) || files.length === 0) return;
|
|
104
|
+
let i = 0
|
|
105
|
+
const worker = async () => {
|
|
106
|
+
while (i < files.length) {
|
|
107
|
+
const f = files[i++]
|
|
108
|
+
if (!f || typeof f !== "object") continue
|
|
109
|
+
const key = dir + "\u0000" + f.path
|
|
110
|
+
if (cache.diffs[key] !== undefined) continue
|
|
111
|
+
try {
|
|
112
|
+
// untracked 新文件 git 不跟踪,无 diff,直接标记为空。
|
|
113
|
+
if (f.type === "untracked") { cache.diffs[key] = ""; continue }
|
|
114
|
+
const r = await jget("/repo-diff?dir=" + encodeURIComponent(dir) + "&path=" + encodeURIComponent(f.path)).catch(() => null)
|
|
115
|
+
cache.diffs[key] = (r && typeof r.diff === "string") ? r.diff : null
|
|
116
|
+
} catch { cache.diffs[key] = null }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
const n = Math.max(1, Math.min(limit || 3, files.length || 1))
|
|
120
|
+
const workers = []
|
|
121
|
+
for (let k = 0; k < n; k++) workers.push(worker())
|
|
122
|
+
await Promise.all(workers)
|
|
123
|
+
}
|
|
124
|
+
|
|
79
125
|
/** 预取 env / ssh / default-dir / repo / workspaces,结果写入 cache。可并发安全。 */
|
|
80
126
|
function preload() {
|
|
81
127
|
if (!preloadPromise) {
|
|
@@ -97,7 +143,19 @@ window.__ModuleLoader__.load({
|
|
|
97
143
|
}
|
|
98
144
|
if (def && def.dir) {
|
|
99
145
|
cache.defDir = def.dir;
|
|
100
|
-
|
|
146
|
+
}
|
|
147
|
+
// 打开 DSH 时即对所有工作区做一次完整同步(full=1 会 git fetch 联网),
|
|
148
|
+
// 这样点开「代码管理」或切换任意工作区都直接显示已同步的最新状态,无需再等。
|
|
149
|
+
// 用并发上限 5 控制,避免一次性对大量仓库同时打满网络。
|
|
150
|
+
const dirs = Array.from(new Set([
|
|
151
|
+
...(cache.workspaces || []),
|
|
152
|
+
...(cache.customDirs || []),
|
|
153
|
+
...(def && def.dir ? [def.dir] : []),
|
|
154
|
+
].filter(Boolean)))
|
|
155
|
+
await syncAllRepos(dirs, 5)
|
|
156
|
+
// 默认工作区的同步结果作为面板首次秒显内容。
|
|
157
|
+
if (def && def.dir && cache.reposByDir[def.dir]) {
|
|
158
|
+
cache.repo = cache.reposByDir[def.dir];
|
|
101
159
|
}
|
|
102
160
|
cache.ready = true;
|
|
103
161
|
} catch {
|
|
@@ -107,6 +165,24 @@ window.__ModuleLoader__.load({
|
|
|
107
165
|
}
|
|
108
166
|
return preloadPromise;
|
|
109
167
|
}
|
|
168
|
+
|
|
169
|
+
/** 并发上限 N 地对所有目录做 full 同步,结果写入 cache.reposByDir[dir]。 */
|
|
170
|
+
async function syncAllRepos(dirs, limit) {
|
|
171
|
+
let i = 0
|
|
172
|
+
const worker = async () => {
|
|
173
|
+
while (i < dirs.length) {
|
|
174
|
+
const d = dirs[i++]
|
|
175
|
+
try {
|
|
176
|
+
const r = await jget("/repo?dir=" + encodeURIComponent(d) + "&full=1").catch(() => null)
|
|
177
|
+
if (r) cache.reposByDir[d] = r
|
|
178
|
+
} catch { /* 单个失败不影响其他 */ }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const workers = []
|
|
182
|
+
const n = Math.max(1, Math.min(limit || 5, dirs.length || 1))
|
|
183
|
+
for (let k = 0; k < n; k++) workers.push(worker())
|
|
184
|
+
await Promise.all(workers)
|
|
185
|
+
}
|
|
110
186
|
/** 忽略缓存强制重新拉取某个资源并更新 cache。 */
|
|
111
187
|
async function refreshCache(key) {
|
|
112
188
|
if (key === "env") cache.env = await jget("/env").catch(() => cache.env);
|
|
@@ -125,9 +201,6 @@ window.__ModuleLoader__.load({
|
|
|
125
201
|
// ---------- small presentational bits ----------
|
|
126
202
|
const h = React.createElement;
|
|
127
203
|
|
|
128
|
-
function Dot({ color }) {
|
|
129
|
-
return h("span", { style: { color, marginRight: 4 } }, "●");
|
|
130
|
-
}
|
|
131
204
|
function Field({ label, value, children }) {
|
|
132
205
|
return h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 6 } },
|
|
133
206
|
h("span", { style: { color: T.secondary, fontSize: 13, width: 84, flexShrink: 0 } }, label),
|
|
@@ -159,20 +232,59 @@ window.__ModuleLoader__.load({
|
|
|
159
232
|
},
|
|
160
233
|
}, label);
|
|
161
234
|
}
|
|
162
|
-
function Box({ title, children }) {
|
|
235
|
+
function Box({ title, badge, defaultCollapsed, collapsible, titleExtra, collapsed: controlledCollapsed, onToggle, children }) {
|
|
236
|
+
// 每个部分默认可折叠:标题行右侧一个「折叠/展开」按钮,点击收起只显示标题。
|
|
237
|
+
// defaultCollapsed 控制初始是否收起(如①环境检查全就绪时默认折叠)。
|
|
238
|
+
// titleExtra:标题行里标题与折叠按钮之间的额外内容(如②的平台切换下拉),
|
|
239
|
+
// 折叠时仍可见、可交互(切换平台会由调用方触发展开)。
|
|
240
|
+
// 受控模式:传入 collapsed/onToggle 时由父组件接管折叠状态(用于「切换标题行控件
|
|
241
|
+
// 后自动展开」);否则用内部 state。
|
|
242
|
+
const [inner, setInner] = useState(!!defaultCollapsed);
|
|
243
|
+
const collapsed = controlledCollapsed !== undefined ? !!controlledCollapsed : inner;
|
|
244
|
+
const setCollapsed = controlledCollapsed !== undefined ? onToggle : setInner;
|
|
245
|
+
const canCollapse = collapsible !== false;
|
|
163
246
|
return h("div", { style: { border: "1px solid " + T.border, borderRadius: 12, padding: 14 } },
|
|
164
|
-
h("div", { style: {
|
|
165
|
-
|
|
247
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" } },
|
|
248
|
+
h("span", { style: { fontSize: 13, fontWeight: 600, color: T.label, flexShrink: 0, minWidth: 0 } }, title),
|
|
249
|
+
titleExtra || null,
|
|
250
|
+
badge ? h("span", { style: { fontSize: 12, color: T.success, flexShrink: 0, whiteSpace: "nowrap" } }, badge) : null,
|
|
251
|
+
canCollapse ? h("span", { style: { flex: 1 } }) : null,
|
|
252
|
+
canCollapse ? h("button", {
|
|
253
|
+
type: "button",
|
|
254
|
+
title: collapsed ? "展开" : "折叠",
|
|
255
|
+
"aria-label": collapsed ? "展开该部分" : "折叠该部分",
|
|
256
|
+
onClick: () => setCollapsed(!collapsed),
|
|
257
|
+
style: collapseBtnStyle(),
|
|
258
|
+
}, collapsed ? "▸" : "▾") : null
|
|
259
|
+
),
|
|
260
|
+
collapsed ? null : h("div", { style: { marginTop: 10 } }, children)
|
|
166
261
|
);
|
|
167
262
|
}
|
|
263
|
+
// 折叠/展开按钮样式(标题行右侧的小圆角按钮)。
|
|
264
|
+
function collapseBtnStyle() {
|
|
265
|
+
return {
|
|
266
|
+
appearance: "none", border: "none", background: "transparent",
|
|
267
|
+
color: T.secondary, cursor: "pointer", fontSize: 14, lineHeight: 1,
|
|
268
|
+
width: 24, height: 24, borderRadius: 6, flexShrink: 0, padding: 0,
|
|
269
|
+
display: "inline-flex", alignItems: "center", justifyContent: "center",
|
|
270
|
+
};
|
|
271
|
+
}
|
|
168
272
|
function pre(code) {
|
|
169
273
|
return h("pre", { style: { whiteSpace: "pre-wrap", fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)", fontSize: 12, lineHeight: 1.6, color: T.secondary, margin: 0 } }, code);
|
|
170
274
|
}
|
|
171
275
|
|
|
172
276
|
// ---------- section 1: 环境检查 ----------
|
|
277
|
+
// 每类工具缺失时给出安装命令 + 「一键安装」按钮(best-effort,走 host 自动选包管理器)。
|
|
278
|
+
const INSTALL_HINTS = {
|
|
279
|
+
git: "winget install --id Git.Git -e",
|
|
280
|
+
gh: "winget install --id GitHub.cli -e",
|
|
281
|
+
ssh: "Add-WindowsCapability -Online -Name OpenSSH.Client",
|
|
282
|
+
};
|
|
173
283
|
function EnvSection() {
|
|
174
284
|
const [env, setEnv] = useState(() => cache.env);
|
|
175
285
|
const [err, setErr] = useState(null);
|
|
286
|
+
const [installing, setInstalling] = useState(null); // 'git'|'gh'|'ssh'|null
|
|
287
|
+
const [installResult, setInstallResult] = useState(null);
|
|
176
288
|
const load = useCallback(async () => {
|
|
177
289
|
setErr(null);
|
|
178
290
|
// 先读缓存(DSH 打开时已预取),再后台刷新保持最新
|
|
@@ -182,16 +294,54 @@ window.__ModuleLoader__.load({
|
|
|
182
294
|
}, []);
|
|
183
295
|
useEffect(() => { void preload().then(load); }, [load]);
|
|
184
296
|
|
|
185
|
-
|
|
297
|
+
// 一键安装:调用 host 选定包管理器安装缺失工具,成功后重新检测。
|
|
298
|
+
const doInstall = useCallback(async (tool) => {
|
|
299
|
+
setInstalling(tool); setInstallResult(null);
|
|
300
|
+
try {
|
|
301
|
+
const r = await jpost("/install-tool", { tool });
|
|
302
|
+
setInstallResult(r);
|
|
303
|
+
if (r.ok) void load();
|
|
304
|
+
} catch (e) { setInstallResult({ ok: false, tool, error: String(e) }); }
|
|
305
|
+
finally { setInstalling(null); }
|
|
306
|
+
}, [load]);
|
|
307
|
+
|
|
308
|
+
// 是否所有工具都就绪(git / gh / ssh 均已安装)——就绪时该部分默认折叠,
|
|
309
|
+
// 只显示「① 环境检查」标题 + 「均存在」提示;否则默认展开让用户看到缺什么。
|
|
310
|
+
const allPresent = !!(cache.env && cache.env.git && cache.env.git.installed
|
|
311
|
+
&& cache.env.gh && cache.env.gh.installed
|
|
312
|
+
&& cache.env.ssh && cache.env.ssh.installed);
|
|
313
|
+
const winOs = (env && env.platform === "win32");
|
|
314
|
+
|
|
315
|
+
// 渲染单个工具行;缺失时带安装命令 + 安装按钮。
|
|
316
|
+
const toolRow = (tool, label, installed, version) => {
|
|
317
|
+
const hint = INSTALL_HINTS[tool] || ""
|
|
318
|
+
const status = installed
|
|
319
|
+
? h("span", { style: { color: T.label, fontSize: 13 } }, "✅ " + (version || "已安装"))
|
|
320
|
+
: h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
321
|
+
h("span", { style: { color: T.danger, fontSize: 13 } }, "❌ 未安装"),
|
|
322
|
+
h("button", { type: "button", title: "复制安装命令", disabled: installing !== null, style: changeBtnStyle(), onClick: (e) => { e.stopPropagation(); void copyText(hint); } }, "复制安装命令"),
|
|
323
|
+
h("button", { type: "button", title: "一键安装(会自动选包管理器)", disabled: installing !== null, style: { ...changeBtnStyle(), color: T.brand, fontWeight: 600 }, onClick: (e) => { e.stopPropagation(); void doInstall(tool); } }, installing === tool ? "安装中…" : "安装")
|
|
324
|
+
)
|
|
325
|
+
const feedback = installResult && installResult.tool === tool
|
|
326
|
+
? (installResult.ok
|
|
327
|
+
? h("div", { style: { marginTop: 4, color: T.success, fontSize: 12 } }, "✅ " + (installResult.command || "已安装") + " 执行成功" + (installResult.needElevation ? "(如未生效需以管理员身份重试)" : ""))
|
|
328
|
+
: h("div", { style: { marginTop: 4, color: T.danger, fontSize: 12 } }, "❌ " + (installResult.error || "安装失败") + (installResult.command ? ":" + installResult.command : "")))
|
|
329
|
+
: null
|
|
330
|
+
return h("div", { key: tool, style: { marginTop: 4 } },
|
|
331
|
+
h(Field, { label }, status),
|
|
332
|
+
feedback
|
|
333
|
+
)
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return h(Box, { title: "① 环境检查", badge: allPresent ? "✅ 均存在" : null, defaultCollapsed: !!allPresent },
|
|
186
337
|
h(Field, { label: "操作系统" }, h("span", { style: { color: T.label, fontSize: 13 } }, env ? (env.platformLabel || env.platform) : "...")),
|
|
187
338
|
env ? h(React.Fragment, null,
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
339
|
+
toolRow("git", "Git", env.git.installed, env.git.version),
|
|
340
|
+
toolRow("gh", "GitHub CLI", env.gh.installed, env.gh.version),
|
|
341
|
+
toolRow("ssh", "SSH", !!(env.ssh && env.ssh.installed), env.ssh && env.ssh.installed ? "已找到" : null)
|
|
191
342
|
) : h(Field, { label: "检测中", value: err ? "失败: " + err : "…" }),
|
|
192
|
-
(!env || !env.git.installed || !env.gh.installed) ? h("div", { style: { marginTop: 8 } },
|
|
193
|
-
|
|
194
|
-
"未安装工具请先安装 Git for Windows 和 GitHub CLI,然后重启。" + (err ? "(" + err + ")" : ""))
|
|
343
|
+
(!env || !env.git.installed || !env.gh.installed || !(env.ssh && env.ssh.installed)) ? h("div", { style: { marginTop: 8, fontSize: 12, color: T.secondary, lineHeight: 1.6 } },
|
|
344
|
+
"未找到的工具可用上方「安装」按钮一键安装(" + (winOs ? "Windows 用 winget / 内置功能" : "用系统包管理器") + ",可能需管理员权限);也可手动复制安装命令执行,安装后点「重新检查」。"
|
|
195
345
|
) : h(Btn, { label: "重新检查", onClick: load, tone: "ghost" })
|
|
196
346
|
);
|
|
197
347
|
}
|
|
@@ -205,6 +355,8 @@ window.__ModuleLoader__.load({
|
|
|
205
355
|
// 平台状态由 ScmPanel 共享(②里选择,③跟随)
|
|
206
356
|
const prov = provider || "github";
|
|
207
357
|
const setProv = setProvider || (() => {});
|
|
358
|
+
// ② 默认折叠;在标题行也能切平台,切换时自动展开(因为要做后续配置)。
|
|
359
|
+
const [collapsed, setCollapsed] = useState(true);
|
|
208
360
|
|
|
209
361
|
const load = useCallback(async () => {
|
|
210
362
|
setErr(null);
|
|
@@ -232,23 +384,30 @@ window.__ModuleLoader__.load({
|
|
|
232
384
|
: !!(ssh && ssh.sshGitHubConfigured);
|
|
233
385
|
const providerHost = prov === "gitee" ? "gitee.com" : "github.com";
|
|
234
386
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
h("
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
},
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
387
|
+
// 标题行的平台切换下拉:折叠时也可见、可交互;切换后自动展开(要做配置)。
|
|
388
|
+
const titleExtra = h("span", { style: { display: "inline-flex", alignItems: "center", gap: 6, flexShrink: 0 } },
|
|
389
|
+
h("span", { style: { fontSize: 12, color: T.secondary } }, "平台"),
|
|
390
|
+
h("select", {
|
|
391
|
+
value: prov,
|
|
392
|
+
onChange: (e) => { setProv(e.target.value); setCollapsed(false); },
|
|
393
|
+
title: "选择代码托管平台:GitHub 或 Gitee(默认 GitHub),③代码管理会跟随切换",
|
|
394
|
+
style: { font: "inherit", fontSize: 12, padding: "3px 6px", border: "1px solid " + T.border, borderRadius: 6, background: T.layer1, color: T.label, outline: "none", cursor: "pointer" },
|
|
395
|
+
},
|
|
396
|
+
h("option", { value: "github" }, "GitHub(默认)"),
|
|
397
|
+
h("option", { value: "gitee" }, "Gitee"),
|
|
398
|
+
)
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
return h(Box, {
|
|
402
|
+
title: "② SSH 密钥与连接",
|
|
403
|
+
defaultCollapsed: true,
|
|
404
|
+
collapsed,
|
|
405
|
+
onToggle: (v) => setCollapsed(v),
|
|
406
|
+
titleExtra,
|
|
407
|
+
},
|
|
408
|
+
// key status(显示实际检测到的密钥名,可能不是 id_ed25519)
|
|
250
409
|
h(Field, { label: "密钥" }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
251
|
-
ssh ? (ssh.hasKey ? "✅ id_ed25519 已生成" : "⚠️ 未生成") : "…")
|
|
410
|
+
ssh ? (ssh.hasKey ? "✅ " + (ssh.keyBase || "id_ed25519") + " 已生成" : "⚠️ 未生成") : "…")
|
|
252
411
|
),
|
|
253
412
|
h(Field, { label: "GH 登录" }, h("span", { style: { color: T.label, fontSize: 13 } },
|
|
254
413
|
ssh ? (ssh.ghLoggedIn ? "✅ " + (ssh.ghAccount || "已登录") : "⚠️ 未登录") : "…")
|
|
@@ -289,6 +448,11 @@ window.__ModuleLoader__.load({
|
|
|
289
448
|
const [dirDraft, setDirDraft] = useState(() => cache.defDir ?? "");
|
|
290
449
|
const [repo, setRepo] = useState(() => cache.repo);
|
|
291
450
|
const [busy, setBusy] = useState(false);
|
|
451
|
+
// 刷新状态(联网同步)进行中的提示状态,避免刷新时界面闪成「未加载文件夹」。
|
|
452
|
+
const [refreshing, setRefreshing] = useState(false);
|
|
453
|
+
// 切换平台(② 里 GitHub/Gitee)时,③ 需要重新拉取对应平台的仓库状态;此标记在拉取期间为真,
|
|
454
|
+
// 用于显示「切换平台,正在重新检测…」提示——避免保留原内容几秒而让用户以为没反应。
|
|
455
|
+
const [switching, setSwitching] = useState(false);
|
|
292
456
|
const [result, setResult] = useState(null);
|
|
293
457
|
const [msg, setMsg] = useState(null);
|
|
294
458
|
const [err, setErr] = useState(null);
|
|
@@ -303,8 +467,30 @@ window.__ModuleLoader__.load({
|
|
|
303
467
|
const [giteeTokenBusy, setGiteeTokenBusy] = useState(false);
|
|
304
468
|
// 自定义目录(用户手动选择的非 DSH 工作区,持久化于插件本地),用于给下拉项加 X 删除
|
|
305
469
|
const [customDirs, setCustomDirs] = useState(() => cache.customDirs || []);
|
|
306
|
-
// 详情弹窗:'changes'(改动文件列表)| 'sync'(同步差异)|
|
|
470
|
+
// 详情弹窗:'changes'(改动文件列表)| 'sync'(同步差异)| 'branches'(分支切换)
|
|
471
|
+
// | 'history'(提交历史)| null(关闭)
|
|
307
472
|
const [detail, setDetail] = useState(null);
|
|
473
|
+
// 改动详情里被展开显示 diff 的文件下标集合(点击文件名展开/收起内容)
|
|
474
|
+
const [expandedDiffs, setExpandedDiffs] = useState(() => new Set());
|
|
475
|
+
// 按需加载的 diff:{ [index]: { loading, diff, error } },点开文件行时才请求 /repo-diff
|
|
476
|
+
const [diffs, setDiffs] = useState({});
|
|
477
|
+
// 提交信息输入框 + 暂存/取消暂存进行中标记
|
|
478
|
+
const [commitMsg, setCommitMsg] = useState("");
|
|
479
|
+
const [stageBusy, setStageBusy] = useState(false);
|
|
480
|
+
// 分支切换弹窗数据 + 进行中标记
|
|
481
|
+
const [branches, setBranches] = useState([]);
|
|
482
|
+
const [branchCurrent, setBranchCurrent] = useState("");
|
|
483
|
+
const [branchBusy, setBranchBusy] = useState(false);
|
|
484
|
+
// 提交历史弹窗数据 + 进行中标记;historyDetail 存放某条 commit 的并排 diff
|
|
485
|
+
const [commits, setCommits] = useState([]);
|
|
486
|
+
const [commitsLoading, setCommitsLoading] = useState(false);
|
|
487
|
+
const [historyBusy, setHistoryBusy] = useState(false);
|
|
488
|
+
// historyDetail:{ hash, loading, diff, error },点某提交时按需拉 /commit-diff
|
|
489
|
+
const [historyDetail, setHistoryDetail] = useState(null);
|
|
490
|
+
// 提交行右键/更多菜单:当前打开的 commit(hashFull)或 null
|
|
491
|
+
const [commitMenu, setCommitMenu] = useState(null);
|
|
492
|
+
// 复制成功的即时提示(菜单里短暂显示「已复制」)
|
|
493
|
+
const [copiedTip, setCopiedTip] = useState(null);
|
|
308
494
|
// 仓库名称强制 = 文件夹名(不允许手动填写)
|
|
309
495
|
const repoName = (repo && repo.defaultRepoName) || "";
|
|
310
496
|
// 目录选择器状态
|
|
@@ -317,13 +503,61 @@ window.__ModuleLoader__.load({
|
|
|
317
503
|
const provRef = useRef(prov);
|
|
318
504
|
provRef.current = prov;
|
|
319
505
|
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
506
|
+
// 竞态保护:loadRepo 是异步的(await 网络),首次挂载的预加载请求可能与
|
|
507
|
+
// 用户手动切换工作区/平台的请求并发。若旧请求后返回,会把它对应的目录
|
|
508
|
+
// 内容(并 setDir 改回旧目录)覆盖到界面上,表现为「切换后过一会儿又跳回
|
|
509
|
+
// 切换前的目录」。用自增 token 保证只有最新一次调用才允许写入状态。
|
|
510
|
+
const loadSeq = useRef(0);
|
|
511
|
+
|
|
512
|
+
// 可见性默认取值:优先用当前仓库的实际私有/公开状态;当该工作区没有远程
|
|
513
|
+
// (属于当前账号/平台的远程、即即将新建的仓库)时,默认选「公开」。供
|
|
514
|
+
// loadRepo 的各个返回路径(网络 / 快速缓存 / 预取秒显)统一调用。
|
|
515
|
+
const syncVisibility = useCallback((r) => {
|
|
516
|
+
if (r && r.repoExists === true && (r.visibility === "public" || r.visibility === "private")) {
|
|
517
|
+
setVisibility(r.visibility);
|
|
518
|
+
} else if (r && !r.hasRemote) {
|
|
519
|
+
setVisibility("public");
|
|
520
|
+
}
|
|
521
|
+
}, []);
|
|
522
|
+
|
|
523
|
+
const loadRepo = useCallback(async (d, keepRepo, full) => {
|
|
524
|
+
// 注意:不能在这里无条件清空 err —— 操作回调(推送/拉取/对齐等)会先
|
|
525
|
+
// setErr/setMsg 再调 loadRepo 刷新状态,此处清空会立刻抹掉刚设置的
|
|
526
|
+
// 结果/错误提示,造成「点了按钮没有任何反馈」。需要清空 err 的调用方
|
|
527
|
+
// (切换目录 / 切换平台 / 刷新状态)自行清空。
|
|
528
|
+
// full=true 时才让 host 端执行 git fetch 联网同步(用于「刷新状态」及
|
|
529
|
+
// 推送/拉取/对齐等操作后的刷新);默认 false(快速模式):跳过 fetch,
|
|
530
|
+
// 秒出本地状态。full 模式自动显示「刷新中…」提示,提示只在最新一次调用归来时关闭。
|
|
531
|
+
const doFull = full === true;
|
|
532
|
+
const mySeq = ++loadSeq.current;
|
|
533
|
+
if (doFull) setRefreshing(true);
|
|
534
|
+
const dirty = !keepRepo;
|
|
535
|
+
if (dirty) setRepo(null);
|
|
536
|
+
// 快速模式且预取已同步过该目录:直接秒显缓存,不再等网络(打开 DSH 时已全部 full 同步)。
|
|
537
|
+
// 注意:缓存是按目录存的,但内容随平台(github/gitee)不同。只有缓存的 provider 与
|
|
538
|
+
// 当前平台一致才可秒显;否则跳过缓存,走网络重新拉取对应平台的仓库状态,避免切换
|
|
539
|
+
// 平台后仍显示上一个平台的检测内容。
|
|
540
|
+
const curProv = provRef.current || "github";
|
|
541
|
+
if (!doFull && cache.reposByDir[d] && cache.reposByDir[d].provider === curProv) {
|
|
542
|
+
const cached = cache.reposByDir[d];
|
|
543
|
+
if (mySeq === loadSeq.current) {
|
|
544
|
+
setRepo(cached);
|
|
545
|
+
if (cached.dir) { setDirDraft(cached.dir); setDir(cached.dir); }
|
|
546
|
+
syncVisibility(cached);
|
|
547
|
+
if (!hasBetterSidebar) void prefetchDiffs(cached.dir, cached.changedFiles, 3);
|
|
548
|
+
}
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
323
551
|
try {
|
|
324
|
-
const p =
|
|
325
|
-
const r = await jget("/repo?dir=" + encodeURIComponent(d) + "&provider=" + encodeURIComponent(p));
|
|
552
|
+
const p = curProv;
|
|
553
|
+
const r = await jget("/repo?dir=" + encodeURIComponent(d) + "&provider=" + encodeURIComponent(p) + (doFull ? "&full=1" : ""));
|
|
554
|
+
// 竞态保护:若期间又发起了更新的 loadRepo(序列号更大),丢弃本次过期结果,
|
|
555
|
+
// 避免旧请求后到时把界面刷回旧目录。
|
|
556
|
+
if (mySeq !== loadSeq.current) return;
|
|
326
557
|
setRepo(r);
|
|
558
|
+
// 切换仓库/刷新后清空旧的改动 diff 缓存与展开状态,避免残留上一个目录的内容。
|
|
559
|
+
setExpandedDiffs(new Set());
|
|
560
|
+
setDiffs({});
|
|
327
561
|
// 默认选中当前工作区:/repo 在未传 dir 时返回当前工作区路径,
|
|
328
562
|
// 把返回的实际目录同步到输入框和当前选中目录。
|
|
329
563
|
if (r && r.dir) {
|
|
@@ -333,32 +567,46 @@ window.__ModuleLoader__.load({
|
|
|
333
567
|
}
|
|
334
568
|
// 缓存最新 repo 状态
|
|
335
569
|
cache.repo = r;
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
if (r && r.
|
|
339
|
-
|
|
340
|
-
|
|
570
|
+
// 按目录缓存(含 provider),下次同一平台可用快速缓存秒显;切平台时因 provider
|
|
571
|
+
// 不匹配会跳过缓存重新拉取,避免显示上个平台的检测内容。
|
|
572
|
+
if (r && r.dir) cache.reposByDir[r.dir] = r;
|
|
573
|
+
// 默认可见性:优先当前仓库实际状态;无远程(即将新建的仓库)默认「公开」。
|
|
574
|
+
syncVisibility(r);
|
|
575
|
+
// 后台预取本目录所有改动文件的 diff,点击「查看」时秒出。
|
|
576
|
+
// 先清掉上次残留的缓存(文件集刚可能变化),再并发拉取。
|
|
577
|
+
clearDiffCache(r && r.dir);
|
|
578
|
+
if (!hasBetterSidebar) void prefetchDiffs(r && r.dir, r && r.changedFiles, 3);
|
|
341
579
|
if (r && !r.isGitRepo) setErr(r.error || "该目录不是 git 仓库");
|
|
342
580
|
} catch (e) { setErr(String(e)); }
|
|
581
|
+
finally {
|
|
582
|
+
// 只有本次是最新调用时才收起「刷新中…」提示,避免旧请求失败把提示提前关掉。
|
|
583
|
+
if (doFull && mySeq === loadSeq.current) setRefreshing(false);
|
|
584
|
+
}
|
|
343
585
|
}, []);
|
|
344
586
|
|
|
345
587
|
useEffect(() => {
|
|
346
|
-
//
|
|
347
|
-
//
|
|
348
|
-
|
|
349
|
-
setRepo(cache.repo);
|
|
350
|
-
if (cache.defDir) { setDir(cache.defDir); setDirDraft(cache.defDir); }
|
|
351
|
-
}
|
|
588
|
+
// 首次加载:优先用 DSH 打开时已 full 同步好的 cache.repo 秒显;
|
|
589
|
+
// 无论预加载是否已在此时完成,都走 preload()(幂等),在 .then 里
|
|
590
|
+
// 直接用预取结果,避免「先显示(未加载文件夹)空白再等一会儿」。
|
|
352
591
|
if (cache.workspaces && cache.workspaces.length) setWorkspaces(cache.workspaces);
|
|
353
592
|
if (cache.customDirs && cache.customDirs.length) setCustomDirs(cache.customDirs);
|
|
354
593
|
void preload().then(() => {
|
|
355
594
|
if (cache.workspaces && cache.workspaces.length) setWorkspaces(cache.workspaces);
|
|
356
595
|
if (cache.customDirs && cache.customDirs.length) setCustomDirs(cache.customDirs);
|
|
357
|
-
// 默认工作区:优先 defDir,否则第一个工作区
|
|
358
596
|
const d = cache.defDir
|
|
359
597
|
|| (cache.workspaces && cache.workspaces[0])
|
|
360
598
|
|| dir || "";
|
|
361
|
-
if (d)
|
|
599
|
+
if (!d) return;
|
|
600
|
+
setDir(d); setDirDraft(d);
|
|
601
|
+
if (cache.repo) {
|
|
602
|
+
// 预加载已做 full 同步,直接秒显,不再发重复请求。
|
|
603
|
+
setRepo(cache.repo);
|
|
604
|
+
syncVisibility(cache.repo);
|
|
605
|
+
if (!hasBetterSidebar) void prefetchDiffs(cache.repo.dir, cache.repo.changedFiles, 3);
|
|
606
|
+
} else {
|
|
607
|
+
// 兜底:预加载未拿到(如请求失败),发一次 full 同步刷新。
|
|
608
|
+
loadRepo(d, true, true);
|
|
609
|
+
}
|
|
362
610
|
});
|
|
363
611
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
364
612
|
}, []);
|
|
@@ -409,7 +657,13 @@ window.__ModuleLoader__.load({
|
|
|
409
657
|
// 平台切换:gitee 时读取令牌状态;无论切到哪都刷新仓库(跟随平台检测/显示)
|
|
410
658
|
useEffect(() => {
|
|
411
659
|
if (prov === "gitee") void loadGiteeTokenStatus();
|
|
412
|
-
|
|
660
|
+
// 切换平台等同切换检测目标,清掉旧的错误提示,避免残留上一个平台的报错。
|
|
661
|
+
setErr(null);
|
|
662
|
+
if (dir || cache.defDir) {
|
|
663
|
+
// 显示「切换平台,正在重新检测…」提示,避免切换后旧内容停留几秒看起来像没变化。
|
|
664
|
+
setSwitching(true);
|
|
665
|
+
loadRepo(dir || cache.defDir || "", true).finally(() => setSwitching(false));
|
|
666
|
+
}
|
|
413
667
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
414
668
|
}, [prov]);
|
|
415
669
|
|
|
@@ -420,19 +674,23 @@ window.__ModuleLoader__.load({
|
|
|
420
674
|
setResult(r);
|
|
421
675
|
if (r.ok) setMsg("已推送");
|
|
422
676
|
else setErr(r.error || r.pushError || "推送失败");
|
|
423
|
-
void loadRepo(dir, true);
|
|
677
|
+
void loadRepo(dir, true, true);
|
|
424
678
|
} catch (e) { setErr("推送失败:" + e); }
|
|
425
679
|
finally { setBusy(false); }
|
|
426
680
|
}, [dir, loadRepo]);
|
|
427
681
|
|
|
428
682
|
const pull = useCallback(async () => {
|
|
683
|
+
// 「拉取更新」在“本地干净 + 远程有更新”状态出现:本地没有任何改动/提交,
|
|
684
|
+
// reset --hard 不会丢失任何内容,因此直接复用「强制对齐」的实现
|
|
685
|
+
// (git fetch + git reset --hard origin/<branch>),且无需弹确认框,
|
|
686
|
+
// 等同于把本地快速前进到远程最新状态。
|
|
429
687
|
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
430
688
|
try {
|
|
431
|
-
const r = await jpost("/
|
|
689
|
+
const r = await jpost("/align", { dir: dir || undefined });
|
|
432
690
|
setResult(r);
|
|
433
|
-
if (r.ok) setMsg(
|
|
691
|
+
if (r.ok) setMsg("已拉取远程更新(本地已对齐到远程最新状态)");
|
|
434
692
|
else setErr(r.error || "拉取失败");
|
|
435
|
-
void loadRepo(dir, true);
|
|
693
|
+
void loadRepo(dir, true, true);
|
|
436
694
|
} catch (e) { setErr("拉取失败:" + e); }
|
|
437
695
|
finally { setBusy(false); }
|
|
438
696
|
}, [dir, loadRepo]);
|
|
@@ -444,7 +702,7 @@ window.__ModuleLoader__.load({
|
|
|
444
702
|
setResult(r);
|
|
445
703
|
if (r.ok) setMsg("已拉取远程更新并推送更改");
|
|
446
704
|
else setErr(r.error || "合并推送失败");
|
|
447
|
-
void loadRepo(dir, true);
|
|
705
|
+
void loadRepo(dir, true, true);
|
|
448
706
|
} catch (e) { setErr("合并推送失败:" + e); }
|
|
449
707
|
finally { setBusy(false); }
|
|
450
708
|
}, [dir, loadRepo]);
|
|
@@ -457,7 +715,7 @@ window.__ModuleLoader__.load({
|
|
|
457
715
|
setResult(r);
|
|
458
716
|
if (r.ok) setMsg("已强制推送,远程已更新为本地状态");
|
|
459
717
|
else setErr(r.error || "强制推送失败");
|
|
460
|
-
void loadRepo(dir, true);
|
|
718
|
+
void loadRepo(dir, true, true);
|
|
461
719
|
} catch (e) { setErr("强制推送失败:" + e); }
|
|
462
720
|
finally { setBusy(false); }
|
|
463
721
|
}, [dir, loadRepo]);
|
|
@@ -470,7 +728,7 @@ window.__ModuleLoader__.load({
|
|
|
470
728
|
setResult(r);
|
|
471
729
|
if (r.ok) setMsg("已强制拉取远程更新");
|
|
472
730
|
else setErr(r.error || "强制拉取失败");
|
|
473
|
-
void loadRepo(dir, true);
|
|
731
|
+
void loadRepo(dir, true, true);
|
|
474
732
|
} catch (e) { setErr("强制拉取失败:" + e); }
|
|
475
733
|
finally { setBusy(false); }
|
|
476
734
|
}, [dir, loadRepo]);
|
|
@@ -486,7 +744,7 @@ window.__ModuleLoader__.load({
|
|
|
486
744
|
setResult(r);
|
|
487
745
|
if (r.ok) setMsg("已把仓库设置为「" + (target === "public" ? "公开" : "私有") + "」");
|
|
488
746
|
else setErr(r.error || "修改可见性失败");
|
|
489
|
-
void loadRepo(dir, true);
|
|
747
|
+
void loadRepo(dir, true, true);
|
|
490
748
|
} catch (e) { setErr("修改可见性失败:" + e); }
|
|
491
749
|
finally { setBusy(false); }
|
|
492
750
|
}, [dir, repo, visibility, loadRepo]);
|
|
@@ -500,7 +758,7 @@ window.__ModuleLoader__.load({
|
|
|
500
758
|
setResult(r);
|
|
501
759
|
if (r.ok) setMsg("仓库已创建并推送:" + (r.url || name) + (r.provider === "gitee" ? "(Gitee)" : ""));
|
|
502
760
|
else setErr(r.error || "创建失败");
|
|
503
|
-
void loadRepo(dir, true);
|
|
761
|
+
void loadRepo(dir, true, true);
|
|
504
762
|
} catch (e) { setErr("创建失败:" + e); }
|
|
505
763
|
finally { setBusy(false); }
|
|
506
764
|
}, [dir, repo, visibility, loadRepo]);
|
|
@@ -600,10 +858,151 @@ window.__ModuleLoader__.load({
|
|
|
600
858
|
finally { setBusy(false); }
|
|
601
859
|
}, [dir, loadRepo]);
|
|
602
860
|
|
|
861
|
+
// ---- 本地 Git 工作流:选择性暂存 / 提交 / 分支 / 历史 / revert / cherry-pick ----
|
|
862
|
+
|
|
863
|
+
// 暂存某文件(path 空 = 全部),成功后刷新 repo 状态。
|
|
864
|
+
const doStage = useCallback(async (path) => {
|
|
865
|
+
setStageBusy(true); setErr(null);
|
|
866
|
+
try {
|
|
867
|
+
const r = await jpost("/stage", { dir: dir || undefined, path: path || undefined });
|
|
868
|
+
if (r.ok) {
|
|
869
|
+
// 暂存会改变文件的 staged 状态:清掉该目录的缓存快照,强制走网络路径
|
|
870
|
+
// 重新拉取最新 repo(含最新「已暂存/未暂存」标记),并清 diff 展开状态。
|
|
871
|
+
if (dir) delete cache.reposByDir[dir];
|
|
872
|
+
loadRepo(dir, true);
|
|
873
|
+
} else setErr(r.error || "暂存失败");
|
|
874
|
+
} catch (e) { setErr("暂存失败:" + e); }
|
|
875
|
+
finally { setStageBusy(false); }
|
|
876
|
+
}, [dir, loadRepo]);
|
|
877
|
+
|
|
878
|
+
// 取消暂存某文件(path 空 = 全部)。
|
|
879
|
+
const doUnstage = useCallback(async (path) => {
|
|
880
|
+
setStageBusy(true); setErr(null);
|
|
881
|
+
try {
|
|
882
|
+
const r = await jpost("/unstage", { dir: dir || undefined, path: path || undefined });
|
|
883
|
+
if (r.ok) {
|
|
884
|
+
if (dir) delete cache.reposByDir[dir];
|
|
885
|
+
loadRepo(dir, true);
|
|
886
|
+
} else setErr(r.error || "取消暂存失败");
|
|
887
|
+
} catch (e) { setErr("取消暂存失败:" + e); }
|
|
888
|
+
finally { setStageBusy(false); }
|
|
889
|
+
}, [dir, loadRepo]);
|
|
890
|
+
|
|
891
|
+
// 用自定义信息提交暂存区;成功后刷新并提示。
|
|
892
|
+
const doCommit = useCallback(async () => {
|
|
893
|
+
const name = repoName || (repo && repo.dir ? String(repo.dir).split(/[\\/]/).filter(Boolean).pop() : "");
|
|
894
|
+
const message = commitMsg.trim() || ("chore: update " + name);
|
|
895
|
+
setBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
896
|
+
try {
|
|
897
|
+
const r = await jpost("/commit", { dir: dir || undefined, message });
|
|
898
|
+
setResult(r);
|
|
899
|
+
if (r.ok) {
|
|
900
|
+
setMsg("已提交 " + r.staged + " 个文件" + (r.hash ? "(" + r.hash + ")" : ""));
|
|
901
|
+
setCommitMsg("");
|
|
902
|
+
void loadRepo(dir, true, true);
|
|
903
|
+
} else setErr(r.error || "提交失败");
|
|
904
|
+
} catch (e) { setErr("提交失败:" + e); }
|
|
905
|
+
finally { setBusy(false); }
|
|
906
|
+
}, [dir, repo, commitMsg, repoName, loadRepo]);
|
|
907
|
+
|
|
908
|
+
// 打开分支切换弹窗。
|
|
909
|
+
const openBranches = useCallback(async () => {
|
|
910
|
+
setDetail("branches"); setBranchBusy(true); setBranches([]);
|
|
911
|
+
try {
|
|
912
|
+
const r = await jpost("/branches", { dir: dir || undefined });
|
|
913
|
+
if (r.ok) { setBranches(r.branches || []); setBranchCurrent(r.current || ""); }
|
|
914
|
+
else setErr(r.error || "读取分支失败");
|
|
915
|
+
} catch (e) { setErr("读取分支失败:" + e); }
|
|
916
|
+
finally { setBranchBusy(false); }
|
|
917
|
+
}, [dir]);
|
|
918
|
+
|
|
919
|
+
// 切换到某分支。
|
|
920
|
+
const doCheckout = useCallback(async (branch) => {
|
|
921
|
+
if (branch === branchCurrent) { setDetail(null); return; }
|
|
922
|
+
if (!window.confirm("切换到分支「" + branch + "」?(若当前有未提交改动,git 会拒绝切换)")) return;
|
|
923
|
+
setBranchBusy(true); setMsg(null); setErr(null);
|
|
924
|
+
try {
|
|
925
|
+
const r = await jpost("/checkout", { dir: dir || undefined, branch });
|
|
926
|
+
if (r.ok) {
|
|
927
|
+
setMsg("已切换到分支 " + branch);
|
|
928
|
+
setDetail(null);
|
|
929
|
+
void loadRepo(dir, true, true);
|
|
930
|
+
} else setErr(r.error || "切换分支失败");
|
|
931
|
+
} catch (e) { setErr("切换分支失败:" + e); }
|
|
932
|
+
finally { setBranchBusy(false); }
|
|
933
|
+
}, [dir, branchCurrent, loadRepo]);
|
|
934
|
+
|
|
935
|
+
// 打开提交历史弹窗。
|
|
936
|
+
const openHistory = useCallback(async () => {
|
|
937
|
+
setDetail("history"); setCommitsLoading(true); setCommits([]); setHistoryDetail(null);
|
|
938
|
+
try {
|
|
939
|
+
const r = await jpost("/log", { dir: dir || undefined, count: 30 });
|
|
940
|
+
if (r.ok) setCommits(r.commits || []);
|
|
941
|
+
else setErr(r.error || "读取历史失败");
|
|
942
|
+
} catch (e) { setErr("读取历史失败:" + e); }
|
|
943
|
+
finally { setCommitsLoading(false); }
|
|
944
|
+
}, [dir]);
|
|
945
|
+
|
|
946
|
+
// 对某提交执行 revert(改写历史,需确认)。
|
|
947
|
+
const doRevert = useCallback(async (hash, subject) => {
|
|
948
|
+
if (!window.confirm("revert 提交「" + (subject || hash) + "」——会生成一个反向提交,期间可能产生冲突,确定继续?")) return;
|
|
949
|
+
setHistoryBusy(true); setMsg(null); setErr(null);
|
|
950
|
+
try {
|
|
951
|
+
const r = await jpost("/revert", { dir: dir || undefined, hash });
|
|
952
|
+
if (r.ok) {
|
|
953
|
+
setMsg("已 revert 提交 " + hash);
|
|
954
|
+
setDetail(null);
|
|
955
|
+
void loadRepo(dir, true, true);
|
|
956
|
+
} else setErr(r.error || "revert 失败");
|
|
957
|
+
} catch (e) { setErr("revert 失败:" + e); }
|
|
958
|
+
finally { setHistoryBusy(false); }
|
|
959
|
+
}, [dir, loadRepo]);
|
|
960
|
+
|
|
961
|
+
// 对某提交执行 cherry-pick(改写历史,需确认)。
|
|
962
|
+
const doCherryPick = useCallback(async (hash, subject) => {
|
|
963
|
+
if (!window.confirm("cherry-pick 提交「" + (subject || hash) + "」到当前分支——期间可能产生冲突,确定继续?")) return;
|
|
964
|
+
setHistoryBusy(true); setMsg(null); setErr(null);
|
|
965
|
+
try {
|
|
966
|
+
const r = await jpost("/cherrypick", { dir: dir || undefined, hash });
|
|
967
|
+
if (r.ok) {
|
|
968
|
+
setMsg("已 cherry-pick 提交 " + hash);
|
|
969
|
+
setDetail(null);
|
|
970
|
+
void loadRepo(dir, true, true);
|
|
971
|
+
} else setErr(r.error || "cherry-pick 失败");
|
|
972
|
+
} catch (e) { setErr("cherry-pick 失败:" + e); }
|
|
973
|
+
finally { setHistoryBusy(false); }
|
|
974
|
+
}, [dir, loadRepo]);
|
|
975
|
+
|
|
976
|
+
// 打开某提交的并排 diff(复用 /commit-diff)。
|
|
977
|
+
const openCommitDiff = useCallback(async (hash) => {
|
|
978
|
+
if (historyDetail && historyDetail.hash === hash) { setHistoryDetail(null); return; }
|
|
979
|
+
setHistoryDetail({ hash, loading: true, diff: "", error: null });
|
|
980
|
+
try {
|
|
981
|
+
const r = await jpost("/commit-diff", { dir: dir || undefined, hash });
|
|
982
|
+
setHistoryDetail({ hash, loading: false, diff: (r && r.diff) || "", error: r && !r.ok ? (r.error || "读取失败") : null });
|
|
983
|
+
} catch (e) {
|
|
984
|
+
setHistoryDetail({ hash, loading: false, diff: "", error: String(e) });
|
|
985
|
+
}
|
|
986
|
+
}, [dir, historyDetail]);
|
|
987
|
+
|
|
988
|
+
// 复制提交相关信息(短哈希 / 完整哈希 / 提交信息),成功后短暂提示并关闭菜单。
|
|
989
|
+
const copyCommit = useCallback(async (kind, commit) => {
|
|
990
|
+
let text = "";
|
|
991
|
+
if (kind === "short") text = commit.hash;
|
|
992
|
+
else if (kind === "full") text = commit.hashFull;
|
|
993
|
+
else text = commit.subject || "";
|
|
994
|
+
const ok = await copyText(text);
|
|
995
|
+
setCommitMenu(null);
|
|
996
|
+
if (ok) {
|
|
997
|
+
setCopiedTip("已复制" + (kind === "short" ? "短哈希" : kind === "full" ? "完整哈希" : "提交信息"));
|
|
998
|
+
window.setTimeout(() => setCopiedTip(null), 1600);
|
|
999
|
+
} else {
|
|
1000
|
+
setErr("复制失败:请手动复制");
|
|
1001
|
+
}
|
|
1002
|
+
}, []);
|
|
1003
|
+
|
|
603
1004
|
return h(Box, { title: "③ 代码管理" },
|
|
604
1005
|
// 平台提示 + Gitee 令牌(仅 gitee 模式)
|
|
605
|
-
h("div", { style: { marginBottom: 10, fontSize: 12, color: T.secondary, lineHeight: 1.6 } },
|
|
606
|
-
"当前平台:<" + (prov === "gitee" ? "Gitee" : "GitHub") + ">(在 ② SSH 里切换,③ 的同名检测 / 新建仓库 / 可见性会跟随)。"),
|
|
607
1006
|
prov === "gitee" ? h("div", { style: { border: "1px solid " + T.border, borderRadius: 10, padding: 10, marginBottom: 10, background: T.layer1 } },
|
|
608
1007
|
h("div", { style: { fontSize: 12, fontWeight: 600, color: T.label, marginBottom: 6 } },
|
|
609
1008
|
"Gitee 私人令牌(OpenAPI,需 projects 权限)"),
|
|
@@ -668,7 +1067,23 @@ window.__ModuleLoader__.load({
|
|
|
668
1067
|
) : null,
|
|
669
1068
|
repo && repo.isGitRepo ? h("div", { style: { marginBottom: 10 } },
|
|
670
1069
|
h(Field, { label: "目录", value: repo.dir }),
|
|
671
|
-
h(Field, { label: "分支",
|
|
1070
|
+
h(Field, { label: "分支" }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
1071
|
+
h("span", { style: { color: T.label, fontSize: 13 } }, repo.branch || "(无)"),
|
|
1072
|
+
!hasBetterSidebar ? h(React.Fragment, null,
|
|
1073
|
+
repo.branch ? h("button", {
|
|
1074
|
+
type: "button",
|
|
1075
|
+
onClick: openBranches,
|
|
1076
|
+
title: "切换分支",
|
|
1077
|
+
style: changeBtnStyle(),
|
|
1078
|
+
}, "切换") : null,
|
|
1079
|
+
h("button", {
|
|
1080
|
+
type: "button",
|
|
1081
|
+
onClick: openHistory,
|
|
1082
|
+
title: "查看提交历史",
|
|
1083
|
+
style: changeBtnStyle(),
|
|
1084
|
+
}, "历史")
|
|
1085
|
+
) : null
|
|
1086
|
+
)),
|
|
672
1087
|
h(Field, { label: "远程", value: repo.remoteUrl || "(无)" }),
|
|
673
1088
|
h(Field, { label: "改动" }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
674
1089
|
h("span", { style: { color: T.label, fontSize: 13 } }, repo.dirty ? repo.dirtyCount + " 个文件" : "无"),
|
|
@@ -682,9 +1097,12 @@ window.__ModuleLoader__.load({
|
|
|
682
1097
|
)),
|
|
683
1098
|
h(Field, { label: "同步" }, h("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 } },
|
|
684
1099
|
h("span", { style: { color: T.label, fontSize: 13 } },
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
1100
|
+
// 没有(属于当前账号/平台的)远程时不做同步判断,避免误显示「与远程一致」
|
|
1101
|
+
!repo.hasRemote
|
|
1102
|
+
? "(无)"
|
|
1103
|
+
: (repo.ahead > 0 ? "本地领先 " + repo.ahead + " 提交" : "") +
|
|
1104
|
+
(repo.ahead > 0 && repo.behind > 0 ? "、落后 " + repo.behind + " 提交" : (repo.behind > 0 ? "落后 " + repo.behind + " 提交" : "")) +
|
|
1105
|
+
((repo.ahead === 0 && repo.behind === 0) ? "与远程一致" : "")
|
|
688
1106
|
),
|
|
689
1107
|
(repo.ahead > 0 || repo.behind > 0) ? h("button", {
|
|
690
1108
|
type: "button",
|
|
@@ -719,30 +1137,57 @@ window.__ModuleLoader__.load({
|
|
|
719
1137
|
btns.push(h(Btn, { key: "init", label: "创建 Git", onClick: initGit, tone: "primary", noBg: true, disabled: isBlocked, title: "仅初始化 git 仓库,不拉取不推送,由你决定下一步" }));
|
|
720
1138
|
} else if (hasRemote) {
|
|
721
1139
|
if (localChanges && remoteUpdates) {
|
|
722
|
-
//
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1140
|
+
// 本地和远程都有更新:原名展示「拉取更新并推送更改 / 强制推送 /
|
|
1141
|
+
// 强制拉取」三按钮,但这三种操作在此场景下容易因未提交改动、
|
|
1142
|
+
// 分支保护等原因失败且行为难预测,故不再展示——该状态唯一的
|
|
1143
|
+
// 操作按钮是「强制对齐」(fetch + reset --hard,本地完全重置为远程)。
|
|
1144
|
+
btns.push(h("span", { key: "both-note", style: { color: T.warn, fontSize: 12 } },
|
|
1145
|
+
"本地有改动且远程有更新,可直接用「强制对齐」将本地重置为远程状态"));
|
|
1146
|
+
if (repo && repo.isGitRepo) {
|
|
1147
|
+
btns.push(h(Btn, { key: "align", label: "强制对齐", onClick: align, disabled: isBlocked, title: "本地完全重置为远程分支(丢弃本地差异),解决“文件相同仍显示同步差异”的情况" }));
|
|
1148
|
+
}
|
|
726
1149
|
} else if (localChanges) {
|
|
727
1150
|
// 只有本地有更改 -> 只显示推送(正常 push)
|
|
728
1151
|
btns.push(h(Btn, { key: "push", label: "推送更改", onClick: push, tone: "primary", noBg: true, disabled: isBlocked || repo?.repoExists === false }));
|
|
729
1152
|
} else if (remoteUpdates) {
|
|
730
|
-
//
|
|
1153
|
+
// 只有远程有更新(本地干净)-> 只显示「拉取更新」。
|
|
1154
|
+
// 实现复用「强制对齐」逻辑(fetch + reset --hard):本地干净
|
|
1155
|
+
// 无改动可丢,reset 等同快速前进到远程最新,直接执行不弹确认。
|
|
731
1156
|
btns.push(h(Btn, { key: "pull", label: "拉取更新", onClick: pull, disabled: isBlocked || repo?.repoExists !== true }));
|
|
732
1157
|
} else {
|
|
733
1158
|
// 完全同步 -> 无按钮,显示已是最新
|
|
734
1159
|
btns.push(h("span", { key: "synced", style: { color: T.success, fontSize: 13 } }, "✓ 已是最新"));
|
|
735
1160
|
}
|
|
736
|
-
// 有远程时总是提供「强制对齐」作为兜底(本地完全重置为远程状态)
|
|
737
|
-
if (repo && repo.isGitRepo) {
|
|
738
|
-
btns.push(h(Btn, { key: "align", label: "强制对齐", onClick: align, disabled: isBlocked, title: "本地完全重置为远程分支(丢弃本地差异),解决“文件相同仍显示同步差异”的情况" }));
|
|
739
|
-
}
|
|
740
1161
|
}
|
|
1162
|
+
// 刷新/切换平台进行中给出明确提示,让用户知道正在联网同步或重新检测。
|
|
1163
|
+
const refreshingTip = refreshing
|
|
1164
|
+
? h("div", { style: { color: T.brand, fontSize: 12, marginTop: 8 } }, "⟳ 正在刷新状态,联网同步 GitHub/Gitee 最新数据…")
|
|
1165
|
+
: switching
|
|
1166
|
+
? h("div", { style: { color: T.brand, fontSize: 12, marginTop: 8 } }, "⟳ 切换平台,正在重新检测「" + (prov === "gitee" ? "Gitee" : "GitHub") + "」仓库状态…")
|
|
1167
|
+
: null
|
|
741
1168
|
return h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" } },
|
|
742
1169
|
btns,
|
|
743
|
-
|
|
1170
|
+
// keepRepo=true 保留旧内容避免闪烁;full=true 触发 loadRepo 的「刷新中…」提示 + 联网同步。
|
|
1171
|
+
h(Btn, {
|
|
1172
|
+
label: refreshing ? "刷新中…" : "刷新状态",
|
|
1173
|
+
onClick: () => { setResult(null); setMsg(null); setErr(null); loadRepo(dir, true, true); },
|
|
1174
|
+
disabled: busy || refreshing,
|
|
1175
|
+
}),
|
|
1176
|
+
refreshingTip,
|
|
744
1177
|
);
|
|
745
1178
|
})(),
|
|
1179
|
+
// 提交信息输入框 + 「提交」(仅 git 仓库且有改动时、且未装 better-sidebar 时显示)。
|
|
1180
|
+
!hasBetterSidebar && repo && repo.isGitRepo && repo.dirty ? h("div", { style: { display: "flex", gap: 8, marginTop: 10, alignItems: "center" } },
|
|
1181
|
+
h("input", {
|
|
1182
|
+
type: "text", value: commitMsg,
|
|
1183
|
+
placeholder: "提交信息(留空则用默认)",
|
|
1184
|
+
title: "填写提交信息后点「提交」;留空则自动生成(chore: update <文件夹名>)",
|
|
1185
|
+
onChange: (e) => setCommitMsg(e.target.value),
|
|
1186
|
+
onKeyDown: (e) => { if (e.key === "Enter") void doCommit(); },
|
|
1187
|
+
style: { flex: 1, font: "inherit", fontSize: 12, padding: "6px 10px", border: "1px solid " + T.border, borderRadius: 8, background: T.layer1, color: T.label, outline: "none" },
|
|
1188
|
+
}),
|
|
1189
|
+
h(Btn, { label: "提交", onClick: doCommit, tone: "primary", noBg: true, disabled: busy || stageBusy || repo.dirtyCount <= 0 || !dir }),
|
|
1190
|
+
) : null,
|
|
746
1191
|
h("div", { style: { display: "flex", gap: 8, marginTop: 10, alignItems: "center" } },
|
|
747
1192
|
h("input", {
|
|
748
1193
|
type: "text", value: repoName, readOnly: true,
|
|
@@ -798,20 +1243,137 @@ window.__ModuleLoader__.load({
|
|
|
798
1243
|
detail ? ReactDOM.createPortal(
|
|
799
1244
|
h("div", { style: { position: "fixed", inset: 0, zIndex: 2000, display: "flex", alignItems: "center", justifyContent: "center" }, role: "presentation" },
|
|
800
1245
|
h("div", { style: { position: "absolute", inset: 0, background: T.mask }, "aria-hidden": "true", onClick: () => setDetail(null) }),
|
|
801
|
-
h("div", { style: { position: "relative", zIndex: 1, width: 520, maxWidth: "calc(100vw - 48px)", maxHeight: "calc(100vh - 100px)", display: "flex", flexDirection: "column", background: "var(--dsw-alias-bg-layer-3, #fff)", border: "1px solid " + T.border, borderRadius: 14, padding: 18, color: T.label, boxShadow: "var(--dsw-overlay-shadow, 0 12px 32px rgba(0,0,0,.35))" }, role: "dialog", "aria-modal": "true", "aria-label": "查看详情" },
|
|
1246
|
+
h("div", { style: { position: "relative", zIndex: 1, width: detail === "changes" ? 700 : 520, maxWidth: "calc(100vw - 48px)", maxHeight: "calc(100vh - 100px)", display: "flex", flexDirection: "column", background: "var(--dsw-alias-bg-layer-3, #fff)", border: "1px solid " + T.border, borderRadius: 14, padding: 18, color: T.label, boxShadow: "var(--dsw-overlay-shadow, 0 12px 32px rgba(0,0,0,.35))" }, role: "dialog", "aria-modal": "true", "aria-label": "查看详情" },
|
|
802
1247
|
h("div", { style: { display: "flex", alignItems: "flex-start", gap: 12, marginBottom: 10 } },
|
|
803
1248
|
h("div", { style: { fontSize: 15, fontWeight: 600, flex: 1 } },
|
|
804
|
-
detail === "changes" ? "改动文件"
|
|
1249
|
+
detail === "changes" ? "改动文件"
|
|
1250
|
+
: detail === "branches" ? "切换分支"
|
|
1251
|
+
: detail === "history" ? "提交历史"
|
|
1252
|
+
: "与远程同步差异"),
|
|
805
1253
|
h("button", { type: "button", style: closeBtnStyle(), "aria-label": "关闭", onClick: () => setDetail(null) }, "✕")
|
|
806
1254
|
),
|
|
807
1255
|
h("div", { style: { flex: 1, overflow: "auto" } },
|
|
808
|
-
detail === "changes" ? (
|
|
1256
|
+
detail === "changes" ? h(React.Fragment, null,
|
|
1257
|
+
hasBetterSidebar ? h("div", { style: { marginBottom: 10, padding: "8px 10px", borderRadius: 8, background: T.layer1, color: T.secondary, fontSize: 12, lineHeight: 1.6 } },
|
|
1258
|
+
"已安装 dsh-better-sidebar,此处只列改动文件列表;具体改动内容请到 dsh-better-sidebar 的「源代码管理 / Git 面板」查看。")
|
|
1259
|
+
: null,
|
|
809
1260
|
repo && repo.changedFiles && repo.changedFiles.length > 0
|
|
810
|
-
? repo.changedFiles.map((f, i) =>
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
1261
|
+
? repo.changedFiles.map((f, i) => {
|
|
1262
|
+
// untracked 新文件 git 不跟踪,没有 diff,不可展开。
|
|
1263
|
+
// 未装 better-sidebar 才能展开 diff(装了它由它的 Git 面板负责,避免重复)。
|
|
1264
|
+
const canHaveDiff = !hasBetterSidebar && f.type !== 'untracked'
|
|
1265
|
+
const open = hasBetterSidebar ? false : expandedDiffs.has(i)
|
|
1266
|
+
const d = diffs[i] || {}
|
|
1267
|
+
const onClick = canHaveDiff ? async () => {
|
|
1268
|
+
const next = new Set(expandedDiffs)
|
|
1269
|
+
if (next.has(i)) { next.delete(i); setExpandedDiffs(next); return }
|
|
1270
|
+
next.add(i); setExpandedDiffs(next)
|
|
1271
|
+
// 优先用预取缓存(点击「查看」秒出);未命中才按需请求并写入缓存。
|
|
1272
|
+
if (!diffs[i]) {
|
|
1273
|
+
const key = (repo.dir || "") + "\u0000" + f.path
|
|
1274
|
+
const cached = cache.diffs[key]
|
|
1275
|
+
// 命中且非失败标记(null=上次预取失败),才秒出;否则重新请求。
|
|
1276
|
+
if (cached !== undefined && cached !== null) {
|
|
1277
|
+
setDiffs(prev => ({ ...prev, [i]: { loading: false, diff: cached, error: null } }))
|
|
1278
|
+
return
|
|
1279
|
+
}
|
|
1280
|
+
setDiffs(prev => ({ ...prev, [i]: { loading: true, diff: '' } }))
|
|
1281
|
+
try {
|
|
1282
|
+
const r = await jget("/repo-diff?dir=" + encodeURIComponent(repo.dir || "") + "&path=" + encodeURIComponent(f.path))
|
|
1283
|
+
const diff = (r && r.diff) || ''
|
|
1284
|
+
cache.diffs[key] = diff
|
|
1285
|
+
setDiffs(prev => ({ ...prev, [i]: { loading: false, diff } }))
|
|
1286
|
+
} catch (e) {
|
|
1287
|
+
cache.diffs[key] = null
|
|
1288
|
+
setDiffs(prev => ({ ...prev, [i]: { loading: false, diff: '', error: String(e) } }))
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
} : undefined
|
|
1292
|
+
return h("div", { key: i },
|
|
1293
|
+
h("div", {
|
|
1294
|
+
style: {
|
|
1295
|
+
display: "flex", gap: 8, alignItems: "center",
|
|
1296
|
+
fontSize: 13, lineHeight: 1.8,
|
|
1297
|
+
borderBottom: "1px solid " + T.border, padding: "3px 2px",
|
|
1298
|
+
cursor: canHaveDiff ? "pointer" : "default",
|
|
1299
|
+
},
|
|
1300
|
+
title: canHaveDiff ? (open ? "点击收起" : "点击查看改动内容") : "新文件(untracked)暂无内容 diff",
|
|
1301
|
+
onClick: onClick,
|
|
1302
|
+
},
|
|
1303
|
+
h("span", { style: { color: typeColor(f.type), fontSize: 11, flexShrink: 0, width: 52 } }, typeLabel(f.type)),
|
|
1304
|
+
h("span", { style: { color: T.label, wordBreak: "break-all", flex: 1 } }, f.path),
|
|
1305
|
+
!hasBetterSidebar ? h(React.Fragment, null,
|
|
1306
|
+
h("span", { style: { fontSize: 11, flexShrink: 0, color: f.staged ? T.success : T.secondary } }, f.staged ? "已暂存" : "未暂存"),
|
|
1307
|
+
h("button", {
|
|
1308
|
+
type: "button",
|
|
1309
|
+
title: f.staged ? "取消暂存" : "暂存",
|
|
1310
|
+
disabled: stageBusy,
|
|
1311
|
+
style: changeBtnStyle(),
|
|
1312
|
+
onClick: (e) => { e.stopPropagation(); if (f.staged) void doUnstage(f.path); else void doStage(f.path); },
|
|
1313
|
+
}, f.staged ? "取消暂存" : "暂存"),
|
|
1314
|
+
canHaveDiff ? h("span", { style: { color: T.brand, fontSize: 11, flexShrink: 0 } },
|
|
1315
|
+
d.loading ? "加载中…" : (open ? "▾ 收起" : "▸ 查看"))
|
|
1316
|
+
: null
|
|
1317
|
+
) : null
|
|
1318
|
+
),
|
|
1319
|
+
open && canHaveDiff ? (
|
|
1320
|
+
d.loading ? h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, "正在加载改动内容…")
|
|
1321
|
+
: d.error ? h("div", { style: { fontSize: 12, color: T.danger, padding: "4px 8px" } }, "❌ " + d.error)
|
|
1322
|
+
: d.diff ? renderSideBySideDiff(d.diff) : h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, "(无内容差异)")
|
|
1323
|
+
) : null
|
|
1324
|
+
);
|
|
1325
|
+
})
|
|
814
1326
|
: h("div", { style: { color: T.secondary, fontSize: 13 } }, "当前没有改动。")
|
|
1327
|
+
) : detail === "branches" ? (
|
|
1328
|
+
branchBusy ? h("div", { style: { color: T.secondary, fontSize: 13 } }, "正在读取分支…")
|
|
1329
|
+
: branches.length === 0 ? h("div", { style: { color: T.secondary, fontSize: 13 } }, "(无分支)")
|
|
1330
|
+
: branches.map((b) =>
|
|
1331
|
+
h("div", { key: b, style: { display: "flex", alignItems: "center", gap: 8, padding: "6px 2px", borderBottom: "1px solid " + T.border } },
|
|
1332
|
+
h("span", { style: { flex: 1, color: b === branchCurrent ? T.success : T.label, fontSize: 13, fontWeight: b === branchCurrent ? 600 : 400 } },
|
|
1333
|
+
b + (b === branchCurrent ? "(当前)" : "")),
|
|
1334
|
+
b !== branchCurrent ? h(Btn, { label: "切换", onClick: () => doCheckout(b), disabled: historyBusy || branchBusy, noBg: true, tone: "primary" }) : null
|
|
1335
|
+
)
|
|
1336
|
+
)
|
|
1337
|
+
) : detail === "history" ? (
|
|
1338
|
+
commitsLoading ? h("div", { style: { color: T.secondary, fontSize: 13 } }, "正在读取历史…")
|
|
1339
|
+
: commits.length === 0 ? h("div", { style: { color: T.secondary, fontSize: 13 } }, "(暂无提交)")
|
|
1340
|
+
: commits.map((c) => {
|
|
1341
|
+
const hd = historyDetail && historyDetail.hash === c.hash ? historyDetail : null
|
|
1342
|
+
const menuOpen = commitMenu === c.hashFull
|
|
1343
|
+
// 菜单项 hover 高亮。
|
|
1344
|
+
const hoverProps = () => ({
|
|
1345
|
+
onMouseEnter: (e) => { e.currentTarget.style.background = T.hover; },
|
|
1346
|
+
onMouseLeave: (e) => { e.currentTarget.style.background = "transparent"; },
|
|
1347
|
+
})
|
|
1348
|
+
return h("div", { key: c.hashFull, style: { borderBottom: "1px solid " + T.border, padding: "6px 2px", position: "relative" }, onContextMenu: (e) => { e.preventDefault(); setCommitMenu(c.hashFull); } },
|
|
1349
|
+
h("div", { style: { display: "flex", alignItems: "center", gap: 8 } },
|
|
1350
|
+
h("span", { style: { fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)", fontSize: 12, color: T.brand, flexShrink: 0 } }, c.hash),
|
|
1351
|
+
h("span", { style: { flex: 1, color: T.label, fontSize: 13, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" } }, c.subject || ""),
|
|
1352
|
+
h("button", { type: "button", title: "更多操作(右键也可打开)", disabled: historyBusy, style: changeBtnStyle(), onClick: (e) => { e.stopPropagation(); setCommitMenu(menuOpen ? null : c.hashFull); } }, "⋯")
|
|
1353
|
+
),
|
|
1354
|
+
h("div", { style: { fontSize: 11, color: T.secondary, marginTop: 2 } },
|
|
1355
|
+
c.author + (c.date ? " · " + c.date : "")),
|
|
1356
|
+
menuOpen ? h("div", { style: menuPanelStyle() },
|
|
1357
|
+
h("div", { ...hoverProps(), style: menuItemStyle(), onClick: () => { setCommitMenu(null); openCommitDiff(c.hash); } },
|
|
1358
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "▸"), "查看提交差异"),
|
|
1359
|
+
h("div", { ...hoverProps(), style: menuItemStyle(), onClick: () => copyCommit("short", c) },
|
|
1360
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "⧉"), "复制短哈希"),
|
|
1361
|
+
h("div", { ...hoverProps(), style: menuItemStyle(), onClick: () => copyCommit("full", c) },
|
|
1362
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "⧉"), "复制完整哈希"),
|
|
1363
|
+
h("div", { ...hoverProps(), style: menuItemStyle(), onClick: () => copyCommit("msg", c) },
|
|
1364
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.secondary } }, "⧉"), "复制提交信息"),
|
|
1365
|
+
h("div", { ...hoverProps(), style: menuItemStyle({ danger: true }), onClick: () => { setCommitMenu(null); doRevert(c.hash, c.subject); } },
|
|
1366
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.danger } }, "↩"), "还原此提交"),
|
|
1367
|
+
h("div", { ...hoverProps(), style: menuItemStyle({ danger: true }), onClick: () => { setCommitMenu(null); doCherryPick(c.hash, c.subject); } },
|
|
1368
|
+
h("span", { style: { width: 16, fontSize: 11, textAlign: "center", color: T.danger } }, "↪"), "拾取此提交")
|
|
1369
|
+
) : null,
|
|
1370
|
+
hd ? (
|
|
1371
|
+
hd.loading ? h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, "正在加载提交 diff…")
|
|
1372
|
+
: hd.error ? h("div", { style: { fontSize: 12, color: T.danger, padding: "4px 8px" } }, "❌ " + hd.error)
|
|
1373
|
+
: hd.diff ? renderSideBySideDiff(hd.diff) : h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, "(无内容差异)")
|
|
1374
|
+
) : null
|
|
1375
|
+
)
|
|
1376
|
+
})
|
|
815
1377
|
) : (
|
|
816
1378
|
h("div", null,
|
|
817
1379
|
repo && (repo.ahead > 0 || repo.behind > 0) ? h("div", null,
|
|
@@ -858,6 +1420,10 @@ window.__ModuleLoader__.load({
|
|
|
858
1420
|
)
|
|
859
1421
|
),
|
|
860
1422
|
document.body
|
|
1423
|
+
) : null,
|
|
1424
|
+
copiedTip ? ReactDOM.createPortal(
|
|
1425
|
+
h("div", { style: { position: "fixed", top: 16, left: "50%", transform: "translateX(-50%)", zIndex: 2147483000, padding: "6px 14px", borderRadius: 8, fontSize: 13, color: "#fff", background: "var(--dsw-alias-state-success-primary, #22c55e)", boxShadow: "var(--dsw-overlay-shadow, 0 4px 12px rgba(0,0,0,.3))" } }, "✅ " + copiedTip),
|
|
1426
|
+
document.body
|
|
861
1427
|
) : null
|
|
862
1428
|
);
|
|
863
1429
|
}
|
|
@@ -876,6 +1442,49 @@ window.__ModuleLoader__.load({
|
|
|
876
1442
|
};
|
|
877
1443
|
}
|
|
878
1444
|
|
|
1445
|
+
/** 提交行「更多操作」下拉面板样式(右对齐、向上弹出,避免在弹窗底部被裁)。 */
|
|
1446
|
+
function menuPanelStyle() {
|
|
1447
|
+
return {
|
|
1448
|
+
position: "absolute", right: 0, bottom: "calc(100% + 4px)", zIndex: 5,
|
|
1449
|
+
width: 180, padding: "4px 0",
|
|
1450
|
+
background: "var(--dsw-alias-bg-layer-3, #fff)",
|
|
1451
|
+
border: "1px solid " + T.border, borderRadius: 10,
|
|
1452
|
+
boxShadow: "var(--dsw-overlay-shadow, 0 8px 24px rgba(0,0,0,.3))",
|
|
1453
|
+
};
|
|
1454
|
+
}
|
|
1455
|
+
/** 下拉菜单单项样式。 */
|
|
1456
|
+
function menuItemStyle({ danger } = {}) {
|
|
1457
|
+
return {
|
|
1458
|
+
display: "flex", alignItems: "center", gap: 8,
|
|
1459
|
+
padding: "7px 12px", fontSize: 13, cursor: "pointer",
|
|
1460
|
+
color: danger ? T.danger : T.label,
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
/** 复制文本到剪贴板(优先异步 Clipboard API,回退 execCommand)。 */
|
|
1465
|
+
function copyText(text) {
|
|
1466
|
+
const str = String(text == null ? "" : text);
|
|
1467
|
+
try {
|
|
1468
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
1469
|
+
return navigator.clipboard.writeText(str).then(() => true).catch(() => fallbackCopy(str));
|
|
1470
|
+
}
|
|
1471
|
+
} catch {}
|
|
1472
|
+
return Promise.resolve(fallbackCopy(str));
|
|
1473
|
+
}
|
|
1474
|
+
function fallbackCopy(str) {
|
|
1475
|
+
try {
|
|
1476
|
+
const ta = document.createElement("textarea");
|
|
1477
|
+
ta.value = str;
|
|
1478
|
+
ta.style.position = "fixed";
|
|
1479
|
+
ta.style.opacity = "0";
|
|
1480
|
+
document.body.appendChild(ta);
|
|
1481
|
+
ta.select();
|
|
1482
|
+
const ok = document.execCommand("copy");
|
|
1483
|
+
ta.remove();
|
|
1484
|
+
return ok;
|
|
1485
|
+
} catch { return false; }
|
|
1486
|
+
}
|
|
1487
|
+
|
|
879
1488
|
/** Chinese label for a changed-file status type. */
|
|
880
1489
|
function typeLabel(t) {
|
|
881
1490
|
return { untracked: "新增", added: "新增", deleted: "删除", renamed: "重命名", modified: "修改" }[t] || "修改";
|
|
@@ -885,14 +1494,95 @@ window.__ModuleLoader__.load({
|
|
|
885
1494
|
return { untracked: T.success, added: T.success, deleted: T.danger, renamed: T.warn, modified: T.brand }[t] || T.label;
|
|
886
1495
|
}
|
|
887
1496
|
|
|
1497
|
+
/**
|
|
1498
|
+
* Render a unified git diff as colored lines: 删除行红色带 -,新增行绿色带 +,
|
|
1499
|
+
* 其余(上下文/文件头/@@ 行)为灰色。逐行解析,保持 monospace 等宽。
|
|
1500
|
+
*/
|
|
1501
|
+
function renderDiff(diff) {
|
|
1502
|
+
if (!diff || typeof diff !== "string") return null;
|
|
1503
|
+
const lines = diff.replace(/\r\n/g, "\n").split("\n");
|
|
1504
|
+
if (lines.length && lines[lines.length - 1] === "") lines.pop();
|
|
1505
|
+
const out = [];
|
|
1506
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1507
|
+
const line = lines[i];
|
|
1508
|
+
let color = T.secondary;
|
|
1509
|
+
if (line.startsWith("+") && !line.startsWith("+++")) color = T.success;
|
|
1510
|
+
else if (line.startsWith("-") && !line.startsWith("---")) color = T.danger;
|
|
1511
|
+
out.push(h("pre", { key: i, style: { margin: 0, padding: "0 6px", fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)", fontSize: 12, lineHeight: 1.6, color, whiteSpace: "pre-wrap", wordBreak: "break-all", background: color === T.success ? "rgba(34,197,94,.10)" : color === T.danger ? "rgba(239,68,68,.10)" : "transparent" } }, line));
|
|
1512
|
+
}
|
|
1513
|
+
return h("div", { style: { background: "rgba(0,0,0,.06)", borderRadius: 8, padding: "6px 2px", margin: "4px 0 8px", maxHeight: 260, overflow: "auto" } }, out);
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* 把 unified diff 文本解析成「并排」行对(左=旧/删除,右=新/新增)。
|
|
1518
|
+
* 按 `@@` 块 + `+`/`-`/` ` 前缀配对:删除行与新增行在同一行左右并排显示,
|
|
1519
|
+
* 上下文行左右相同;`` 与文件头(---/+++ /diff)跳过。
|
|
1520
|
+
* @returns {Array<{ left: string|null, right: string|null, oldNo?: string, newNo?: string, type: 'del'|'add'|'ctx' }>}
|
|
1521
|
+
*/
|
|
1522
|
+
function parseUnifiedDiff(diff) {
|
|
1523
|
+
if (!diff || typeof diff !== "string") return [];
|
|
1524
|
+
const lines = diff.replace(/\r\n/g, "\n").split("\n");
|
|
1525
|
+
const rows = [];
|
|
1526
|
+
let oldNo = 0, newNo = 0;
|
|
1527
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1528
|
+
const line = lines[i];
|
|
1529
|
+
// 跳过文件头 / 块头。
|
|
1530
|
+
if (line.startsWith("@@")) {
|
|
1531
|
+
const m = /-(\d+)(?:,\d+)?\s+\+(\d+)(?:,\d+)?/.exec(line);
|
|
1532
|
+
if (m) { oldNo = parseInt(m[1], 10) || 1; newNo = parseInt(m[2], 10) || 1; }
|
|
1533
|
+
continue;
|
|
1534
|
+
}
|
|
1535
|
+
if (line.startsWith("---") || line.startsWith("+++") || line.startsWith("diff ") || line.startsWith("index ")) continue;
|
|
1536
|
+
if (line.startsWith("\\")) continue; // ""
|
|
1537
|
+
const c = line.charAt(0);
|
|
1538
|
+
if (c === "-") {
|
|
1539
|
+
rows.push({ left: line.slice(1), right: "", type: "del", oldNo: oldNo++, newNo: "" });
|
|
1540
|
+
} else if (c === "+") {
|
|
1541
|
+
rows.push({ left: "", right: line.slice(1), type: "add", oldNo: "", newNo: newNo++ });
|
|
1542
|
+
} else if (c === " ") {
|
|
1543
|
+
rows.push({ left: line.slice(1), right: line.slice(1), type: "ctx", oldNo: oldNo++, newNo: newNo++ });
|
|
1544
|
+
} else {
|
|
1545
|
+
// 非 diff 行(如无前缀段):当上下文处理。
|
|
1546
|
+
rows.push({ left: line, right: line, type: "ctx", oldNo: oldNo++, newNo: newNo++ });
|
|
1547
|
+
}
|
|
1548
|
+
}
|
|
1549
|
+
return rows;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
/** 并排 diff 渲染器:删除行红底居左、新增行绿底居右,上下文两列相同。 */
|
|
1553
|
+
function renderSideBySideDiff(diff) {
|
|
1554
|
+
const rows = parseUnifiedDiff(diff);
|
|
1555
|
+
if (rows.length === 0) return renderDiff(diff);
|
|
1556
|
+
const mono = { fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)", fontSize: 12, lineHeight: 1.55, whiteSpace: "pre-wrap", wordBreak: "break-all" };
|
|
1557
|
+
const cellBg = { del: "rgba(239,68,68,.12)", add: "rgba(34,197,94,.12)", ctx: "transparent" };
|
|
1558
|
+
const gutter = { ...mono, fontSize: 10, lineHeight: 1.55, color: T.secondary, textAlign: "right", padding: "0 6px", whiteSpace: "nowrap", wordBreak: "normal", opacity: 0.7 };
|
|
1559
|
+
return h("div", { style: { border: "1px solid " + T.border, borderRadius: 8, overflow: "hidden", margin: "4px 0 8px", maxHeight: 280, overflowY: "auto", background: "rgba(0,0,0,.04)" } },
|
|
1560
|
+
h("div", { style: { display: "flex", borderBottom: "1px solid " + T.border, position: "sticky", top: 0, background: "var(--dsw-alias-bg-layer-3, #fff)", zIndex: 1 } },
|
|
1561
|
+
h("div", { style: { flex: 1, padding: "3px 8px", fontSize: 12, fontWeight: 600, color: T.danger } }, "旧版本"),
|
|
1562
|
+
h("div", { style: { flex: 1, padding: "3px 8px", fontSize: 12, fontWeight: 600, color: T.success } }, "新版本")
|
|
1563
|
+
),
|
|
1564
|
+
rows.map((r, i) =>
|
|
1565
|
+
h("div", { key: i, style: { display: "flex", background: cellBg[r.type] || "transparent", borderBottom: r.type === "ctx" ? "none" : "1px solid rgba(128,128,128,.1)" } },
|
|
1566
|
+
h("div", { style: { width: 34, flexShrink: 0, ...gutter } }, r.oldNo || ""),
|
|
1567
|
+
h("div", { style: { flex: 1, padding: "0 0 0 2px", color: r.type === "del" ? T.danger : T.label } }, h("pre", { style: { margin: 0, ...mono } }, r.left || " ")),
|
|
1568
|
+
h("div", { style: { width: 34, flexShrink: 0, ...gutter } }, r.newNo || ""),
|
|
1569
|
+
h("div", { style: { flex: 1, padding: "0 0 0 2px", color: r.type === "add" ? T.success : r.type === "del" ? "rgba(128,128,128,.6)" : T.label } }, h("pre", { style: { margin: 0, ...mono } }, r.right || " "))
|
|
1570
|
+
)
|
|
1571
|
+
)
|
|
1572
|
+
);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
888
1575
|
// ---------- the main panel ----------
|
|
889
|
-
|
|
1576
|
+
// `variant` 决定布局:'drawer'(右侧栏抽屉,显示关闭按钮)与 'tab'
|
|
1577
|
+
// (作为 dsh-better-sidebar 侧边栏 Tab 内容,填充容器、隐藏关闭按钮)。
|
|
1578
|
+
function ScmPanel({ onClose, variant }) {
|
|
890
1579
|
// 代码托管平台选择在②里操作、在③里跟随:lift 到面板级别共享。
|
|
891
1580
|
const [provider, setProvider] = useState("github");
|
|
892
|
-
|
|
1581
|
+
const embedded = variant === "tab" || variant === "drawer";
|
|
1582
|
+
return h("div", { style: panelStyle(variant), role: "dialog", "aria-modal": embedded ? undefined : "true", "aria-label": "源代码管理" },
|
|
893
1583
|
h("div", { style: { display: "flex", alignItems: "flex-start", gap: 12 } },
|
|
894
1584
|
h("h2", { style: { margin: 0, fontSize: 16, fontWeight: 600, lineHeight: 1.4, flex: 1 } }, "源代码管理"),
|
|
895
|
-
h("button", { type: "button", style: closeBtnStyle(T), "aria-label": "关闭", onClick: onClose }, "✕")
|
|
1585
|
+
embedded ? null : h("button", { type: "button", style: closeBtnStyle(T), "aria-label": "关闭", onClick: onClose }, "✕")
|
|
896
1586
|
),
|
|
897
1587
|
h("p", { style: { margin: "6px 0 14px", color: T.secondary, fontSize: 12, lineHeight: 1.6 } },
|
|
898
1588
|
"按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理。推送会自动忽略 >100MB 的文件(" + (provider === "gitee" ? "Gitee" : "GitHub") + " 限制)并说明原因。"),
|
|
@@ -904,7 +1594,18 @@ window.__ModuleLoader__.load({
|
|
|
904
1594
|
);
|
|
905
1595
|
}
|
|
906
1596
|
|
|
907
|
-
function panelStyle() {
|
|
1597
|
+
function panelStyle(variant) {
|
|
1598
|
+
// 'tab' / 'drawer':填充宿主容器(不设固定宽高、无边框阴影圆角),由外层
|
|
1599
|
+
// (dsh-better-sidebar 的 Tab 区或右侧抽屉)负责尺寸与表面。
|
|
1600
|
+
if (variant === "tab" || variant === "drawer") {
|
|
1601
|
+
return {
|
|
1602
|
+
position: "relative", display: "flex", flexDirection: "column", gap: 4,
|
|
1603
|
+
width: "100%", maxWidth: "100%", height: "100%", maxHeight: "100%",
|
|
1604
|
+
boxSizing: "border-box", overflow: "auto",
|
|
1605
|
+
padding: variant === "tab" ? 16 : 20, borderRadius: 0,
|
|
1606
|
+
background: "transparent", border: "none", color: T.label,
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
908
1609
|
return {
|
|
909
1610
|
position: "relative", zIndex: 1, display: "flex", flexDirection: "column", gap: 4,
|
|
910
1611
|
width: 620, maxWidth: "calc(100vw - 48px)",
|
|
@@ -918,19 +1619,15 @@ window.__ModuleLoader__.load({
|
|
|
918
1619
|
return { appearance: "none", border: "none", background: "transparent", color: T.secondary, cursor: "pointer", fontSize: 18, lineHeight: 1, padding: "2px 6px", borderRadius: 6 };
|
|
919
1620
|
}
|
|
920
1621
|
|
|
921
|
-
function overlayStyle() {
|
|
922
|
-
return { position: "fixed", inset: 0, zIndex: 1000, display: "flex", alignItems: "center", justifyContent: "center" };
|
|
923
|
-
}
|
|
924
1622
|
function maskStyle() {
|
|
925
1623
|
return { position: "absolute", inset: 0, background: T.mask, backdropFilter: "var(--dsw-mask-blur, blur(4px))" };
|
|
926
1624
|
}
|
|
927
1625
|
|
|
928
|
-
|
|
929
|
-
function
|
|
930
|
-
const
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
viewBox: "0 0 16 16", width: wide ? 16 : 18, height: wide ? 16 : 18,
|
|
1626
|
+
/** 共享的「仓库 / 分支」小图标:既用于右上角浮动按钮,也用作侧边栏 Tab 图标。 */
|
|
1627
|
+
function repoIcon(size) {
|
|
1628
|
+
const s = size || 18;
|
|
1629
|
+
return h("svg", {
|
|
1630
|
+
viewBox: "0 0 16 16", width: s, height: s,
|
|
934
1631
|
fill: "none", stroke: "currentColor", strokeWidth: 1.5,
|
|
935
1632
|
strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true",
|
|
936
1633
|
},
|
|
@@ -938,55 +1635,133 @@ window.__ModuleLoader__.load({
|
|
|
938
1635
|
h("rect", { x: "1.5", y: "2.5", width: "9", height: "11", rx: "1.5" }),
|
|
939
1636
|
h("path", { d: "M14 6.5v3.5a2 2 0 0 1-2 2H5.5" })
|
|
940
1637
|
);
|
|
1638
|
+
}
|
|
941
1639
|
|
|
942
|
-
|
|
1640
|
+
/** 右上角浮动按钮样式(未安装 dsh-better-sidebar 时的入口)。 */
|
|
1641
|
+
function floatBtnStyle() {
|
|
1642
|
+
return {
|
|
1643
|
+
position: "fixed", top: 12, right: 16, zIndex: 2147483000,
|
|
943
1644
|
display: "inline-flex", alignItems: "center", gap: 6,
|
|
944
|
-
height:
|
|
945
|
-
background:
|
|
1645
|
+
height: 32, padding: "0 12px", border: "none", borderRadius: 16,
|
|
1646
|
+
background: T.brand, color: "#fff", cursor: "pointer",
|
|
946
1647
|
fontSize: 13, lineHeight: 1,
|
|
1648
|
+
boxShadow: "var(--dsw-overlay-shadow, 0 4px 12px rgba(0,0,0,.3))",
|
|
947
1649
|
};
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
/** 右侧栏抽屉外层样式:右对齐、全高、固定宽度。 */
|
|
1653
|
+
function drawerStyle() {
|
|
1654
|
+
return {
|
|
1655
|
+
position: "relative", zIndex: 1, height: "100%", width: 640, maxWidth: "100vw",
|
|
1656
|
+
marginLeft: "auto", display: "flex", flexDirection: "column",
|
|
1657
|
+
boxSizing: "border-box", padding: 0,
|
|
1658
|
+
background: "var(--dsw-alias-bg-layer-3, #fff)", color: T.label,
|
|
1659
|
+
borderLeft: "1px solid " + T.border,
|
|
1660
|
+
boxShadow: "var(--dsw-overlay-shadow, 0 12px 32px rgba(0,0,0,.35))",
|
|
952
1661
|
};
|
|
1662
|
+
}
|
|
953
1663
|
|
|
1664
|
+
// ---------- branch B:右上角浮动按钮 + 右侧抽屉入口 ----------
|
|
1665
|
+
// 未安装 dsh-better-sidebar 时使用:一个紧贴右上角的浮动按钮,点击后展开
|
|
1666
|
+
// 一个右侧栏抽屉(形态类似 dsh-better-sidebar 的右栏),内容放现有 ScmPanel。
|
|
1667
|
+
function ScmFloatingEntry() {
|
|
1668
|
+
const [open, setOpen] = useState(false);
|
|
954
1669
|
return h(React.Fragment, null,
|
|
955
1670
|
h("button", {
|
|
956
1671
|
type: "button",
|
|
957
|
-
style:
|
|
1672
|
+
style: floatBtnStyle(),
|
|
958
1673
|
title: "源代码管理",
|
|
959
1674
|
"aria-label": "源代码管理",
|
|
960
1675
|
onClick: () => setOpen(true),
|
|
961
|
-
|
|
962
|
-
onMouseLeave: (e) => { e.currentTarget.style.background = "transparent"; e.currentTarget.style.color = T.secondary; },
|
|
963
|
-
},
|
|
964
|
-
icon,
|
|
965
|
-
wide ? h("span", { style: { fontSize: 13 } }, "代码管理") : null
|
|
966
|
-
),
|
|
1676
|
+
}, repoIcon(16), h("span", { style: { fontSize: 13 } }, "代码管理")),
|
|
967
1677
|
open ? ReactDOM.createPortal(
|
|
968
|
-
h("div", { style:
|
|
1678
|
+
h("div", { style: { position: "fixed", inset: 0, zIndex: 1000 }, role: "presentation" },
|
|
969
1679
|
h("div", { style: maskStyle(), "aria-hidden": "true", onClick: () => setOpen(false) }),
|
|
970
|
-
h(
|
|
1680
|
+
h("div", { style: drawerStyle() },
|
|
1681
|
+
h(ScmPanel, { variant: "drawer", onClose: () => setOpen(false) })
|
|
1682
|
+
)
|
|
971
1683
|
),
|
|
972
1684
|
document.body
|
|
973
1685
|
) : null
|
|
974
1686
|
);
|
|
975
1687
|
}
|
|
976
1688
|
|
|
1689
|
+
/** 挂载一个经典脚本 React 根(React 18 用 createRoot,老版本回退 render)。 */
|
|
1690
|
+
function mountClientRoot(container, element) {
|
|
1691
|
+
try {
|
|
1692
|
+
if (ReactDOM.createRoot) {
|
|
1693
|
+
const root = ReactDOM.createRoot(container);
|
|
1694
|
+
root.render(element);
|
|
1695
|
+
return root;
|
|
1696
|
+
}
|
|
1697
|
+
if (ReactDOM.render) {
|
|
1698
|
+
ReactDOM.render(element, container);
|
|
1699
|
+
return { unmount: () => ReactDOM.unmountComponentAtNode(container) };
|
|
1700
|
+
}
|
|
1701
|
+
} catch (e) { console.error("[source-code-mgmt] mount failed:", e); }
|
|
1702
|
+
return null;
|
|
1703
|
+
}
|
|
1704
|
+
|
|
977
1705
|
// ---------- cordis plugin body ----------
|
|
978
|
-
|
|
1706
|
+
// 不写死 inject:better-sidebar 是可选集成,未安装时绝不能因为缺服务而让本
|
|
1707
|
+
// 插件报错。改在 apply 里用 ctx.get('betterSidebar') 判空——一次性属性读取,
|
|
1708
|
+
// 零 I/O、零网络,毫秒级,不影响 DSH 启动速度。
|
|
1709
|
+
const inject = [];
|
|
1710
|
+
|
|
1711
|
+
// 是否已安装(并激活)dsh-better-sidebar:决定「③代码管理」是否隐藏本插件自带的
|
|
1712
|
+
// 本地 Git 工作流(暂存/提交/分支/历史/并排 diff)——这些能力 better-sidebar 的
|
|
1713
|
+
// Git 面板已覆盖,装了它就不重复展示。React 组件通过这个模块级标记读取。
|
|
1714
|
+
let hasBetterSidebar = false;
|
|
979
1715
|
|
|
980
1716
|
function apply(ctx) {
|
|
981
1717
|
// DSH 打开(插件激活)时就预取环境/SSH/默认工作区/仓库状态,
|
|
982
1718
|
// 点开「代码管理」面板时直接使用缓存,无需重新加载。
|
|
983
1719
|
void preload();
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
1720
|
+
|
|
1721
|
+
// better-sidebar 是可选集成:若本插件先于它激活,第一次读取会拿到 undefined;
|
|
1722
|
+
// 这里用一次性重试保证无论激活顺序如何,最终都能正确落到分支 A(注册 Tab)。
|
|
1723
|
+
let entryUnmount = null;
|
|
1724
|
+
hasBetterSidebar = false;
|
|
1725
|
+
|
|
1726
|
+
const tryRegisterTab = () => {
|
|
1727
|
+
const bs = typeof ctx.get === "function" ? ctx.get("betterSidebar") : undefined;
|
|
1728
|
+
if (!bs || typeof bs.registerTab !== "function") return false;
|
|
1729
|
+
// 分支 A:已安装 dsh-better-sidebar —— 把「代码管理」注册成它的侧边栏新 Tab
|
|
1730
|
+
// 页面。ctx.effect 保证 HMR / 插件卸载时自动注销该 Tab。
|
|
1731
|
+
hasBetterSidebar = true;
|
|
1732
|
+
ctx.effect(() => bs.registerTab({
|
|
1733
|
+
id: PLUGIN_ID,
|
|
1734
|
+
title: "代码管理",
|
|
1735
|
+
icon: (size) => repoIcon(size),
|
|
1736
|
+
order: 50,
|
|
1737
|
+
single: true,
|
|
1738
|
+
component: () => h(ScmPanel, { variant: "tab" }),
|
|
1739
|
+
}));
|
|
1740
|
+
// 若此前已挂载了分支 B 的浮动按钮,立即拆除它。
|
|
1741
|
+
if (entryUnmount) { entryUnmount(); entryUnmount = null; }
|
|
1742
|
+
return true;
|
|
1743
|
+
};
|
|
1744
|
+
|
|
1745
|
+
if (tryRegisterTab()) return;
|
|
1746
|
+
|
|
1747
|
+
// 分支 B:此刻未检测到 dsh-better-sidebar —— 右上角浮动按钮 + 右侧栏抽屉。
|
|
1748
|
+
if (!ReactDOM) return;
|
|
1749
|
+
ctx.effect(() => {
|
|
1750
|
+
const hostEl = document.createElement("div");
|
|
1751
|
+
hostEl.setAttribute("data-source-code-mgmt-entry", "");
|
|
1752
|
+
document.body.appendChild(hostEl);
|
|
1753
|
+
const root = mountClientRoot(hostEl, h(ScmFloatingEntry));
|
|
1754
|
+
entryUnmount = () => {
|
|
1755
|
+
try { if (root && typeof root.unmount === "function") root.unmount(); } catch {}
|
|
1756
|
+
hostEl.remove();
|
|
1757
|
+
};
|
|
1758
|
+
return () => { if (entryUnmount) { entryUnmount(); entryUnmount = null; } };
|
|
1759
|
+
});
|
|
1760
|
+
|
|
1761
|
+
// 兜底重试一次:better-sidebar 若在本插件之后激活,延迟补注册 Tab 并拆除浮动按钮。
|
|
1762
|
+
// 单次 setTimeout,无轮询、零 I/O,几乎不耗资源;插件卸载时清除。
|
|
1763
|
+
const retryTimer = window.setTimeout(() => { tryRegisterTab(); }, 1500);
|
|
1764
|
+
ctx.effect(() => () => window.clearTimeout(retryTimer));
|
|
990
1765
|
}
|
|
991
1766
|
|
|
992
1767
|
exports.name = PLUGIN_ID;
|