source-code-mgmt 1.5.0 → 1.8.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 +26 -10
- package/lib/client.js +206 -132
- package/lib/index.js +63 -0
- package/package.json +38 -38
package/README.md
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
# source-code-mgmt — DSH 源代码管理插件
|
|
2
2
|
|
|
3
|
-
> 版本:**v1.
|
|
3
|
+
> 版本:**v1.8.0** | 更新日志见文末「[版本历史](#版本历史)」
|
|
4
4
|
|
|
5
5
|
> DSH Web GUI 源代码管理插件:把「环境检查 → SSH 配置 → 代码上传推送」整合进「代码管理」面板,支持 GitHub / Gitee 双平台,一键管理代码仓库。
|
|
6
6
|
|
|
7
|
-
> 入口位置自适应:**已安装 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 时**,「代码管理」作为它侧边栏的一个新 Tab 页面出现(全新侧边栏 Tab
|
|
7
|
+
> 入口位置自适应:**已安装 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 时**,「代码管理」作为它侧边栏的一个新 Tab 页面出现(全新侧边栏 Tab);**未安装时**,「代码管理」按钮出现在 DSH 页面**右上角 Session log 旁边**(同一右对齐列表、同款胶囊样式、间距 8px 不挤在一起),点击后打开一个 **dsh-better-sidebar 外观的右侧集成面板**(推挤主内容区)。两种形态都复用同一套面板 UI,且**不再占用左栏底部按钮**。
|
|
8
8
|
|
|
9
9
|
## 功能
|
|
10
10
|
|
|
11
11
|
集成入口(二选一,自动检测,无需手动切换):
|
|
12
12
|
|
|
13
13
|
- **已安装 dsh-better-sidebar**:「代码管理」注册为它侧边栏的一个**新 Tab 页面**,点击侧边栏 Tab 直接打开面板;
|
|
14
|
-
- **未安装 dsh-better-sidebar
|
|
14
|
+
- **未安装 dsh-better-sidebar**:「代码管理」按钮出现在 DSH 页面**右上角 Session log 旁边的右对齐列表**里(通过 DSH 的 `conversation.session.header.utilities` 槽位注册,与 Session log 同款胶囊样式、间距 8px 不挤在一起),点击后打开一个 **dsh-better-sidebar 外观的右侧集成面板**(内容放同一面板),并把主内容区往左推挤。
|
|
15
15
|
|
|
16
16
|
> 检测只是激活时一次内存读取(`ctx.get('betterSidebar')`),零 I/O、零网络,不影响 DSH 启动速度;两种形态间自动切换,**不再占用左栏底部的按钮**。
|
|
17
17
|
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
- Gitee:用令牌调 Gitee OpenAPI `POST /user/repos` 建仓,再设置 SSH 远程 `git@gitee.com:<owner>/<name>.git` 并 `git push`(走 ② 已配的 SSH 密钥)
|
|
55
55
|
- **>100MB 文件处理**:自动识别超过 100MB 单文件限制的文件——文件在一级子目录内则**忽略整个一级目录**(该文件夹为一整体),根目录独立文件则**忽略单个文件**;已存在于 `.gitignore` 的不重复添加,并显示「未上传原因」
|
|
56
56
|
|
|
57
|
-
###
|
|
58
|
-
DSH
|
|
57
|
+
### 数据加载时机(打开时联网、显示刷新中)
|
|
58
|
+
DSH 打开时**不联网同步仓库**,只预取静态的环境/SSH/工作区列表。**打开插件、切换工作区、刷新状态、以及推送/拉取/暂存/提交等操作后**,都会联网获取对应工作区的最新仓库状态,并显示「⟳ 刷新中…」提示——避免打开/重开/切换时显示可能过期的旧数据(如旧的「无改动」)。关闭面板再打开也会重新同步,不会停留在旧状态。
|
|
59
59
|
|
|
60
60
|
## 安装
|
|
61
61
|
|
|
@@ -93,7 +93,7 @@ dsh web
|
|
|
93
93
|
name: 'source-code-mgmt'
|
|
94
94
|
```
|
|
95
95
|
|
|
96
|
-
保存后**完全重启 dsh web**(不是刷新页面,而是要停掉旧进程后重新启动),然后浏览器 **F5 刷新**,「代码管理」入口即出现(已装 dsh-better-sidebar 时为侧边栏 Tab
|
|
96
|
+
保存后**完全重启 dsh web**(不是刷新页面,而是要停掉旧进程后重新启动),然后浏览器 **F5 刷新**,「代码管理」入口即出现(已装 dsh-better-sidebar 时为侧边栏 Tab,未装时为右上角 Session log 旁的「代码管理」按钮 + 右侧集成面板)。
|
|
97
97
|
|
|
98
98
|
> 用命令直接追加(幂等,已存在则跳过)——PowerShell:
|
|
99
99
|
> ```powershell
|
|
@@ -165,7 +165,7 @@ dsh web
|
|
|
165
165
|
1. **依赖已写入**:`~/.dsh/profiles/web/package.json` 的 `dependencies` 里应有 `source-code-mgmt`。
|
|
166
166
|
2. **符号链接已建立(`link:` 方式)**:`~/.dsh/profiles/web/node_modules/source-code-mgmt` 指向源码目录(Windows 显示为 Junction)。
|
|
167
167
|
3. **激活条目已添加**:`~/.dsh/profiles/web/cordis.patch.yml` 里有 `source-code-mgmt` 的 insert 条目。
|
|
168
|
-
4. **重启后入口可见**:已装 dsh-better-sidebar 时侧边栏出现「代码管理」Tab
|
|
168
|
+
4. **重启后入口可见**:已装 dsh-better-sidebar 时侧边栏出现「代码管理」Tab;未装时右上角 Session log 旁出现「代码管理」按钮,点击展开右侧集成面板。
|
|
169
169
|
|
|
170
170
|
### 常见排障
|
|
171
171
|
|
|
@@ -178,8 +178,8 @@ dsh web
|
|
|
178
178
|
## 使用步骤
|
|
179
179
|
|
|
180
180
|
1. 重启 dsh web 并刷新浏览器
|
|
181
|
-
2. 点击「**代码管理**」入口(已装 dsh-better-sidebar 时点侧边栏 Tab
|
|
182
|
-
3.
|
|
181
|
+
2. 点击「**代码管理**」入口(已装 dsh-better-sidebar 时点侧边栏 Tab,未装时点右上角 Session log 旁的「代码管理」按钮展开右侧集成面板)
|
|
182
|
+
3. 面板打开(联网获取当前工作区最新状态,显示「⟳ 刷新中…」,拉取完成即显示内容)
|
|
183
183
|
4. ①确认 Git / GitHub CLI 已安装 → ②生成密钥并测试连接 → ③选择工作区后推送或新建仓库
|
|
184
184
|
|
|
185
185
|
## 后端 API 路由
|
|
@@ -250,7 +250,23 @@ pnpm add link:$(pwd)
|
|
|
250
250
|
|
|
251
251
|
## 版本历史
|
|
252
252
|
|
|
253
|
-
### v1.
|
|
253
|
+
### v1.8.0(当前)
|
|
254
|
+
本次更新:
|
|
255
|
+
|
|
256
|
+
- **新增「推送暂存」按钮**(③面板「同步」行):当**本地领先有提交**或**有已暂存的改动**时,「同步」行「本地领先 N 提交」后面出现「**推送暂存**」按钮——点击后**只把已暂存的内容**用**你填写的提交信息**(没填则自动生成 `chore: update <文件夹名>`)提交,然后**推送到远程**;不会像「推送更改」那样自动暂存所有未暂存的改动。推送成功后自动刷新状态并显示「⟳ 刷新中…」,界面显示本次提交的信息与 hash。host 端新增 `POST /push-staged` 路由(`pushStagedFlow`,沿用本插件 `run()`/GIT_SSH)
|
|
257
|
+
|
|
258
|
+
### v1.7.0(历史)
|
|
259
|
+
本次更新:
|
|
260
|
+
|
|
261
|
+
- **数据加载时机重做:打开时联网、显示「刷新中」**:此前打开 DSH 会预取所有工作区的仓库状态并缓存,导致打开/重开/切换面板时命中旧缓存,显示可能过期的数据(如旧的「无改动」)。现在改为——**打开 DSH 时不联网同步仓库**(只预取静态的 env/SSH/工作区列表);**打开插件、切换工作区、自定义目录点击、刷新状态、以及推送/拉取/暂存/取消暂存/提交/对齐/初始化等操作后**,一律**联网获取对应工作区的最新状态**,并显示「⟳ 刷新中…」提示(刷新期间按钮变「刷新中…」、状态区显示同步中)。关闭面板再打开也会重新同步,不再停留在旧状态。删除了预取所有工作区的 `syncAllRepos` 与「命中缓存秒显」分支
|
|
262
|
+
|
|
263
|
+
### v1.6.0(历史)
|
|
264
|
+
本次更新:
|
|
265
|
+
|
|
266
|
+
- **未安装 dsh-better-sidebar 时的入口改为「右上角 Session log 旁 + 右侧集成面板」**:原「右上角浮动按钮 + 右侧抽屉」改为——「代码管理」按钮经 DSH 的 `conversation.session.header.utilities` 槽位注册,**出现在右上角 Session log 旁边的右对齐列表**里(与 Session log 同款胶囊样式、间距 8px 不挤在一起);点击后在**右侧展开一个 dsh-better-sidebar 外观的集成面板**(复用原话术:环境检查 / SSH / 代码管理三步),并把主内容区 `#root` **往左推挤**(margin-right + width calc)。slots 服务不可用时降级为右上角浮动按钮 + 右侧面板。改动仅限 `lib/client.js`(浏览器端),刷新页面即生效,host 端 `/api` 路由未动
|
|
267
|
+
- **已安装 dsh-better-sidebar 的形态不变**:仍注册为它侧边栏的「代码管理」Tab;安装/未安装两形态依旧自动检测切换
|
|
268
|
+
|
|
269
|
+
### v1.5.0(历史)
|
|
254
270
|
本次更新:
|
|
255
271
|
|
|
256
272
|
- **① 环境检查缺工具一键安装**:某个工具(git / gh / ssh)未检测到时,该行显示「❌ 未安装」+「复制安装命令」+「**安装**」按钮——「安装」由 host 自动选包管理器执行(Windows winget / 内置功能、macOS brew、Linux apt/dnf/pacman),安装后自动重新检测;host 新增 `POST /install-tool` 路由(best-effort,回传执行命令与输出)
|
package/lib/client.js
CHANGED
|
@@ -7,9 +7,13 @@
|
|
|
7
7
|
*
|
|
8
8
|
* - branch A: dsh-better-sidebar installed → register「代码管理」as a new
|
|
9
9
|
* sidebar Tab page through `ctx.betterSidebar.registerTab`.
|
|
10
|
-
* - branch B: not installed →
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* - branch B: not installed → register a「代码管理」button into the DSH
|
|
11
|
+
* `conversation.session.header.utilities` slot (the right-aligned list that
|
|
12
|
+
* holds "Session log"), so it sits beside "Session log" with the same pill
|
|
13
|
+
* look and 8px gap; clicking opens a RIGHT-side integrated panel (pushing
|
|
14
|
+
* #root left, dsh-better-sidebar style) holding the existing panel.
|
|
15
|
+
* If the `slots` service is unavailable, it falls back to a floating
|
|
16
|
+
* top-right button opening the same right-side panel.
|
|
13
17
|
*
|
|
14
18
|
* The old left-rail bottom button (`sidebar.footer.action` slot) is removed.
|
|
15
19
|
* Either way clicking the entry opens a "源代码管理" panel with:
|
|
@@ -71,14 +75,15 @@ window.__ModuleLoader__.load({
|
|
|
71
75
|
}
|
|
72
76
|
|
|
73
77
|
// ---------- preload cache ----------
|
|
74
|
-
// DSH
|
|
75
|
-
//
|
|
78
|
+
// 只在打开插件时按需拉取仓库状态;DSH 打开(插件激活)时只预取静态的 env / ssh /
|
|
79
|
+
// workspaces / 默认工作区,绝不联网同步仓库——避免打开 DSH 就 fetch、并把可能过期的
|
|
80
|
+
// 数据缓存起来,导致打开/重开面板时显示旧状态(如旧的「无改动」)。
|
|
76
81
|
const cache = {
|
|
77
82
|
env: null,
|
|
78
83
|
ssh: null,
|
|
79
84
|
defDir: null,
|
|
80
85
|
repo: null,
|
|
81
|
-
//
|
|
86
|
+
// 各工作区的最近一次结果(dir -> repo 状态),仅作展示历史,不再用于「秒显无刷新」。
|
|
82
87
|
reposByDir: {},
|
|
83
88
|
// 单文件改动 diff 缓存("dir\u0000path" -> diff 文本),点击「查看」秒出。
|
|
84
89
|
diffs: {},
|
|
@@ -122,7 +127,8 @@ window.__ModuleLoader__.load({
|
|
|
122
127
|
await Promise.all(workers)
|
|
123
128
|
}
|
|
124
129
|
|
|
125
|
-
/** 预取 env / ssh / default-dir /
|
|
130
|
+
/** 预取 env / ssh / default-dir / workspaces(仅静态资源),结果写入 cache。可并发安全。
|
|
131
|
+
* 注意:仓库状态不在此预取(需要用户打开面板/切换工作区时才按需联网获取,并显示刷新中)。 */
|
|
126
132
|
function preload() {
|
|
127
133
|
if (!preloadPromise) {
|
|
128
134
|
preloadPromise = (async () => {
|
|
@@ -144,19 +150,6 @@ window.__ModuleLoader__.load({
|
|
|
144
150
|
if (def && def.dir) {
|
|
145
151
|
cache.defDir = def.dir;
|
|
146
152
|
}
|
|
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];
|
|
159
|
-
}
|
|
160
153
|
cache.ready = true;
|
|
161
154
|
} catch {
|
|
162
155
|
/* 保持部分缓存 */
|
|
@@ -166,23 +159,6 @@ window.__ModuleLoader__.load({
|
|
|
166
159
|
return preloadPromise;
|
|
167
160
|
}
|
|
168
161
|
|
|
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
|
-
}
|
|
186
162
|
/** 忽略缓存强制重新拉取某个资源并更新 cache。 */
|
|
187
163
|
async function refreshCache(key) {
|
|
188
164
|
if (key === "env") cache.env = await jget("/env").catch(() => cache.env);
|
|
@@ -448,6 +424,8 @@ window.__ModuleLoader__.load({
|
|
|
448
424
|
const [dirDraft, setDirDraft] = useState(() => cache.defDir ?? "");
|
|
449
425
|
const [repo, setRepo] = useState(() => cache.repo);
|
|
450
426
|
const [busy, setBusy] = useState(false);
|
|
427
|
+
// 「推送暂存」进行中标记:把已暂存的内容用填写的(或自动生成的)信息提交后推送。
|
|
428
|
+
const [pushStagedBusy, setPushStagedBusy] = useState(false);
|
|
451
429
|
// 刷新状态(联网同步)进行中的提示状态,避免刷新时界面闪成「未加载文件夹」。
|
|
452
430
|
const [refreshing, setRefreshing] = useState(false);
|
|
453
431
|
// 切换平台(② 里 GitHub/Gitee)时,③ 需要重新拉取对应平台的仓库状态;此标记在拉取期间为真,
|
|
@@ -526,28 +504,15 @@ window.__ModuleLoader__.load({
|
|
|
526
504
|
// 结果/错误提示,造成「点了按钮没有任何反馈」。需要清空 err 的调用方
|
|
527
505
|
// (切换目录 / 切换平台 / 刷新状态)自行清空。
|
|
528
506
|
// full=true 时才让 host 端执行 git fetch 联网同步(用于「刷新状态」及
|
|
529
|
-
//
|
|
530
|
-
//
|
|
507
|
+
// 推送/拉取/对齐等操作后的刷新);full 模式自动显示「刷新中…」提示,
|
|
508
|
+
// 提示只在最新一次调用归来时关闭。**始终走网络获取最新仓库状态,绝不命中
|
|
509
|
+
// 缓存秒显**——否则打开/重开/切换工作区会显示旧数据(如旧的「无改动」)。
|
|
531
510
|
const doFull = full === true;
|
|
532
511
|
const mySeq = ++loadSeq.current;
|
|
533
512
|
if (doFull) setRefreshing(true);
|
|
534
513
|
const dirty = !keepRepo;
|
|
535
514
|
if (dirty) setRepo(null);
|
|
536
|
-
// 快速模式且预取已同步过该目录:直接秒显缓存,不再等网络(打开 DSH 时已全部 full 同步)。
|
|
537
|
-
// 注意:缓存是按目录存的,但内容随平台(github/gitee)不同。只有缓存的 provider 与
|
|
538
|
-
// 当前平台一致才可秒显;否则跳过缓存,走网络重新拉取对应平台的仓库状态,避免切换
|
|
539
|
-
// 平台后仍显示上一个平台的检测内容。
|
|
540
515
|
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
|
-
}
|
|
551
516
|
try {
|
|
552
517
|
const p = curProv;
|
|
553
518
|
const r = await jget("/repo?dir=" + encodeURIComponent(d) + "&provider=" + encodeURIComponent(p) + (doFull ? "&full=1" : ""));
|
|
@@ -565,10 +530,8 @@ window.__ModuleLoader__.load({
|
|
|
565
530
|
setDir(r.dir);
|
|
566
531
|
cache.defDir = r.dir;
|
|
567
532
|
}
|
|
568
|
-
//
|
|
533
|
+
// 记录最新 repo 状态(仅作展示历史,绝不再用于「无刷新秒显」)。
|
|
569
534
|
cache.repo = r;
|
|
570
|
-
// 按目录缓存(含 provider),下次同一平台可用快速缓存秒显;切平台时因 provider
|
|
571
|
-
// 不匹配会跳过缓存重新拉取,避免显示上个平台的检测内容。
|
|
572
535
|
if (r && r.dir) cache.reposByDir[r.dir] = r;
|
|
573
536
|
// 默认可见性:优先当前仓库实际状态;无远程(即将新建的仓库)默认「公开」。
|
|
574
537
|
syncVisibility(r);
|
|
@@ -585,9 +548,9 @@ window.__ModuleLoader__.load({
|
|
|
585
548
|
}, []);
|
|
586
549
|
|
|
587
550
|
useEffect(() => {
|
|
588
|
-
//
|
|
589
|
-
//
|
|
590
|
-
//
|
|
551
|
+
// 首次加载:只预取静态的 env/ssh/workspaces/默认目录;仓库状态一律在面板打开后
|
|
552
|
+
// 通过 loadRepo(..., true) 联网获取最新(并显示「刷新中…」),绝不命中可能过期的
|
|
553
|
+
// 缓存——这样关闭面板再打开、或切换工作区,都会重新同步,不会停留在旧的「无改动」。
|
|
591
554
|
if (cache.workspaces && cache.workspaces.length) setWorkspaces(cache.workspaces);
|
|
592
555
|
if (cache.customDirs && cache.customDirs.length) setCustomDirs(cache.customDirs);
|
|
593
556
|
void preload().then(() => {
|
|
@@ -598,15 +561,8 @@ window.__ModuleLoader__.load({
|
|
|
598
561
|
|| dir || "";
|
|
599
562
|
if (!d) return;
|
|
600
563
|
setDir(d); setDirDraft(d);
|
|
601
|
-
|
|
602
|
-
|
|
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
|
-
}
|
|
564
|
+
// 始终 full 同步(联网 fetch + 显示刷新中),获取当前工作区的最新状态。
|
|
565
|
+
loadRepo(d, true, true);
|
|
610
566
|
});
|
|
611
567
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
612
568
|
}, []);
|
|
@@ -632,7 +588,7 @@ window.__ModuleLoader__.load({
|
|
|
632
588
|
setGiteeToken("");
|
|
633
589
|
setGiteeTokenMsg("已保存 Gitee 令牌(账号:" + (r.owner || "?") + ")");
|
|
634
590
|
// 令牌就绪后刷新仓库,让同名检测等按 Gitee 生效
|
|
635
|
-
loadRepo(dir || cache.defDir || "", true);
|
|
591
|
+
loadRepo(dir || cache.defDir || "", true, true);
|
|
636
592
|
} else {
|
|
637
593
|
setGiteeConfigured(false);
|
|
638
594
|
setGiteeTokenErr(r.error || "保存令牌失败");
|
|
@@ -649,7 +605,7 @@ window.__ModuleLoader__.load({
|
|
|
649
605
|
setGiteeOwnerName("");
|
|
650
606
|
setGiteeToken("");
|
|
651
607
|
setGiteeTokenMsg(r.ok ? "已清除 Gitee 令牌" : "清除失败");
|
|
652
|
-
loadRepo(dir || cache.defDir || "", true);
|
|
608
|
+
loadRepo(dir || cache.defDir || "", true, true);
|
|
653
609
|
} catch (e) { setGiteeTokenErr("清除令牌失败:" + e); }
|
|
654
610
|
finally { setGiteeTokenBusy(false); }
|
|
655
611
|
}, [dir, loadRepo]);
|
|
@@ -662,7 +618,7 @@ window.__ModuleLoader__.load({
|
|
|
662
618
|
if (dir || cache.defDir) {
|
|
663
619
|
// 显示「切换平台,正在重新检测…」提示,避免切换后旧内容停留几秒看起来像没变化。
|
|
664
620
|
setSwitching(true);
|
|
665
|
-
loadRepo(dir || cache.defDir || "", true).finally(() => setSwitching(false));
|
|
621
|
+
loadRepo(dir || cache.defDir || "", true, true).finally(() => setSwitching(false));
|
|
666
622
|
}
|
|
667
623
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
668
624
|
}, [prov]);
|
|
@@ -796,7 +752,7 @@ window.__ModuleLoader__.load({
|
|
|
796
752
|
// 切换到新目录,清空上一个目录的操作结果/提示。
|
|
797
753
|
setResult(null); setMsg(null); setErr(null);
|
|
798
754
|
setPickOpen(false);
|
|
799
|
-
loadRepo(r.dir);
|
|
755
|
+
loadRepo(r.dir, true, true);
|
|
800
756
|
} else {
|
|
801
757
|
setPickErr(r.error || "添加目录失败");
|
|
802
758
|
}
|
|
@@ -822,7 +778,7 @@ window.__ModuleLoader__.load({
|
|
|
822
778
|
if (dir === path) {
|
|
823
779
|
const next = (r.workspaces && r.workspaces[0]) || "";
|
|
824
780
|
setDir(next); setDirDraft(next);
|
|
825
|
-
if (next) loadRepo(next); else setRepo(null);
|
|
781
|
+
if (next) loadRepo(next, true, true); else setRepo(null);
|
|
826
782
|
}
|
|
827
783
|
} else {
|
|
828
784
|
setErr(r.error || "删除失败");
|
|
@@ -840,7 +796,7 @@ window.__ModuleLoader__.load({
|
|
|
840
796
|
setResult(r);
|
|
841
797
|
if (r.ok) setMsg("已强制对齐到远程分支");
|
|
842
798
|
else setErr(r.error || "强制对齐失败");
|
|
843
|
-
void loadRepo(dir);
|
|
799
|
+
void loadRepo(dir, true, true);
|
|
844
800
|
} catch (e) { setErr("强制对齐失败:" + e); }
|
|
845
801
|
finally { setBusy(false); }
|
|
846
802
|
}, [dir, loadRepo]);
|
|
@@ -853,7 +809,7 @@ window.__ModuleLoader__.load({
|
|
|
853
809
|
setResult(r);
|
|
854
810
|
if (r.ok) setMsg(r.alreadyRepo ? "已是 git 仓库" : "已创建 git 仓库,请自行拉取或推送");
|
|
855
811
|
else setErr(r.error || "创建 git 失败");
|
|
856
|
-
void loadRepo(dir);
|
|
812
|
+
void loadRepo(dir, true, true);
|
|
857
813
|
} catch (e) { setErr("创建 git 失败:" + e); }
|
|
858
814
|
finally { setBusy(false); }
|
|
859
815
|
}, [dir, loadRepo]);
|
|
@@ -869,7 +825,7 @@ window.__ModuleLoader__.load({
|
|
|
869
825
|
// 暂存会改变文件的 staged 状态:清掉该目录的缓存快照,强制走网络路径
|
|
870
826
|
// 重新拉取最新 repo(含最新「已暂存/未暂存」标记),并清 diff 展开状态。
|
|
871
827
|
if (dir) delete cache.reposByDir[dir];
|
|
872
|
-
loadRepo(dir, true);
|
|
828
|
+
loadRepo(dir, true, true);
|
|
873
829
|
} else setErr(r.error || "暂存失败");
|
|
874
830
|
} catch (e) { setErr("暂存失败:" + e); }
|
|
875
831
|
finally { setStageBusy(false); }
|
|
@@ -882,7 +838,7 @@ window.__ModuleLoader__.load({
|
|
|
882
838
|
const r = await jpost("/unstage", { dir: dir || undefined, path: path || undefined });
|
|
883
839
|
if (r.ok) {
|
|
884
840
|
if (dir) delete cache.reposByDir[dir];
|
|
885
|
-
loadRepo(dir, true);
|
|
841
|
+
loadRepo(dir, true, true);
|
|
886
842
|
} else setErr(r.error || "取消暂存失败");
|
|
887
843
|
} catch (e) { setErr("取消暂存失败:" + e); }
|
|
888
844
|
finally { setStageBusy(false); }
|
|
@@ -905,6 +861,26 @@ window.__ModuleLoader__.load({
|
|
|
905
861
|
finally { setBusy(false); }
|
|
906
862
|
}, [dir, repo, commitMsg, repoName, loadRepo]);
|
|
907
863
|
|
|
864
|
+
// 「推送暂存」:把已暂存的内容用填写的(或自动生成的)信息提交后推送远程。
|
|
865
|
+
// 与「推送更改」不同:它只提交暂存区、沿用自定义信息,不自动 add 全部改动。
|
|
866
|
+
const doPushStaged = useCallback(async () => {
|
|
867
|
+
setPushStagedBusy(true); setMsg(null); setErr(null); setResult(null);
|
|
868
|
+
try {
|
|
869
|
+
const r = await jpost("/push-staged", { dir: dir || undefined, message: commitMsg.trim() });
|
|
870
|
+
setResult(r);
|
|
871
|
+
if (r.ok) {
|
|
872
|
+
const info = r.committed
|
|
873
|
+
? "已提交「" + (r.message || "(自动生成)") + "」并推送" + (r.commitHash ? "(" + r.commitHash + ")" : "")
|
|
874
|
+
: "已推送本地已有的提交";
|
|
875
|
+
setMsg(info);
|
|
876
|
+
setCommitMsg("");
|
|
877
|
+
} else setErr(r.error || r.pushError || "推送失败");
|
|
878
|
+
// 推送成功后刷新状态(full 联网同步,显示「刷新中…」)。
|
|
879
|
+
void loadRepo(dir, true, true);
|
|
880
|
+
} catch (e) { setErr("推送失败:" + e); }
|
|
881
|
+
finally { setPushStagedBusy(false); }
|
|
882
|
+
}, [dir, commitMsg, loadRepo]);
|
|
883
|
+
|
|
908
884
|
// 打开分支切换弹窗。
|
|
909
885
|
const openBranches = useCallback(async () => {
|
|
910
886
|
setDetail("branches"); setBranchBusy(true); setBranches([]);
|
|
@@ -1040,7 +1016,8 @@ window.__ModuleLoader__.load({
|
|
|
1040
1016
|
setResult(null); setMsg(null); setErr(null);
|
|
1041
1017
|
setDir(v);
|
|
1042
1018
|
setDirDraft(v);
|
|
1043
|
-
|
|
1019
|
+
// full 同步:联网获取切换后工作区的最新状态,并显示「刷新中…」。
|
|
1020
|
+
loadRepo(v, true, true);
|
|
1044
1021
|
},
|
|
1045
1022
|
title: "选择 DSH 已登记的工作区文件夹",
|
|
1046
1023
|
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", cursor: "pointer" },
|
|
@@ -1055,7 +1032,7 @@ window.__ModuleLoader__.load({
|
|
|
1055
1032
|
),
|
|
1056
1033
|
dir && customDirs && customDirs.includes(dir) ? h("div", { style: { display: "flex", flexWrap: "wrap", gap: 6, marginBottom: 10, alignItems: "center" } },
|
|
1057
1034
|
h("span", { style: { color: T.secondary, fontSize: 12, whiteSpace: "nowrap" } }, "自定义目录:"),
|
|
1058
|
-
h("span", { key: dir, style: { display: "inline-flex", alignItems: "center", gap: 4, padding: "2px 8px", border: "1px solid " + T.border, borderRadius: 12, fontSize: 12, background: T.layer1, color: T.label, cursor: "pointer" }, title: dir, onClick: () => { setResult(null); setMsg(null); setErr(null); setDir(dir); setDirDraft(dir); loadRepo(dir); } },
|
|
1035
|
+
h("span", { key: dir, style: { display: "inline-flex", alignItems: "center", gap: 4, padding: "2px 8px", border: "1px solid " + T.border, borderRadius: 12, fontSize: 12, background: T.layer1, color: T.label, cursor: "pointer" }, title: dir, onClick: () => { setResult(null); setMsg(null); setErr(null); setDir(dir); setDirDraft(dir); loadRepo(dir, true, true); } },
|
|
1059
1036
|
String(dir).split(/[\\/]/).filter(Boolean).pop() || dir,
|
|
1060
1037
|
h("span", {
|
|
1061
1038
|
role: "button", "aria-label": "删除该目录记录",
|
|
@@ -1167,6 +1144,17 @@ window.__ModuleLoader__.load({
|
|
|
1167
1144
|
: null
|
|
1168
1145
|
return h("div", { style: { display: "flex", gap: 8, flexWrap: "wrap", alignItems: "center" } },
|
|
1169
1146
|
btns,
|
|
1147
|
+
// 「推送暂存」:放在「推送更改」与「刷新状态」之间,按需显示——
|
|
1148
|
+
// 有远程且「本地领先有提交」或「有已暂存的改动」时才出现(未装 better-sidebar 时)。
|
|
1149
|
+
(!hasBetterSidebar && repo && repo.hasRemote
|
|
1150
|
+
&& (repo.ahead > 0 || ((repo.changedFiles || []).some((f) => f.staged)))) ? h(Btn, {
|
|
1151
|
+
key: "push-staged",
|
|
1152
|
+
label: pushStagedBusy ? "推送中…" : "推送暂存",
|
|
1153
|
+
onClick: doPushStaged,
|
|
1154
|
+
tone: "primary", noBg: true,
|
|
1155
|
+
disabled: busy || pushStagedBusy || !dir,
|
|
1156
|
+
title: "把已暂存的内容用填写的(或自动生成的)信息提交后推送到远程;不会自动暂存其他未暂存的改动",
|
|
1157
|
+
}) : null,
|
|
1170
1158
|
// keepRepo=true 保留旧内容避免闪烁;full=true 触发 loadRepo 的「刷新中…」提示 + 联网同步。
|
|
1171
1159
|
h(Btn, {
|
|
1172
1160
|
label: refreshing ? "刷新中…" : "刷新状态",
|
|
@@ -1573,7 +1561,7 @@ window.__ModuleLoader__.load({
|
|
|
1573
1561
|
}
|
|
1574
1562
|
|
|
1575
1563
|
// ---------- the main panel ----------
|
|
1576
|
-
// `variant` 决定布局:'drawer'
|
|
1564
|
+
// `variant` 决定布局:'drawer'(右侧栏集成面板,显示关闭按钮、填充容器)与 'tab'
|
|
1577
1565
|
// (作为 dsh-better-sidebar 侧边栏 Tab 内容,填充容器、隐藏关闭按钮)。
|
|
1578
1566
|
function ScmPanel({ onClose, variant }) {
|
|
1579
1567
|
// 代码托管平台选择在②里操作、在③里跟随:lift 到面板级别共享。
|
|
@@ -1582,7 +1570,7 @@ window.__ModuleLoader__.load({
|
|
|
1582
1570
|
return h("div", { style: panelStyle(variant), role: "dialog", "aria-modal": embedded ? undefined : "true", "aria-label": "源代码管理" },
|
|
1583
1571
|
h("div", { style: { display: "flex", alignItems: "flex-start", gap: 12 } },
|
|
1584
1572
|
h("h2", { style: { margin: 0, fontSize: 16, fontWeight: 600, lineHeight: 1.4, flex: 1 } }, "源代码管理"),
|
|
1585
|
-
|
|
1573
|
+
variant === "tab" ? null : h("button", { type: "button", style: closeBtnStyle(T), "aria-label": "关闭", onClick: onClose }, "✕")
|
|
1586
1574
|
),
|
|
1587
1575
|
h("p", { style: { margin: "6px 0 14px", color: T.secondary, fontSize: 12, lineHeight: 1.6 } },
|
|
1588
1576
|
"按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理。推送会自动忽略 >100MB 的文件(" + (provider === "gitee" ? "Gitee" : "GitHub") + " 限制)并说明原因。"),
|
|
@@ -1595,8 +1583,8 @@ window.__ModuleLoader__.load({
|
|
|
1595
1583
|
}
|
|
1596
1584
|
|
|
1597
1585
|
function panelStyle(variant) {
|
|
1598
|
-
// 'tab' / 'drawer'
|
|
1599
|
-
//
|
|
1586
|
+
// 'tab' / 'drawer':填充宿主容器(不设固定宽高、无边框阴影圆角),
|
|
1587
|
+
// 由外层(dsh-better-sidebar 的 Tab 区或本插件的右侧面板)负责尺寸与表面。
|
|
1600
1588
|
if (variant === "tab" || variant === "drawer") {
|
|
1601
1589
|
return {
|
|
1602
1590
|
position: "relative", display: "flex", flexDirection: "column", gap: 4,
|
|
@@ -1619,11 +1607,31 @@ window.__ModuleLoader__.load({
|
|
|
1619
1607
|
return { appearance: "none", border: "none", background: "transparent", color: T.secondary, cursor: "pointer", fontSize: 18, lineHeight: 1, padding: "2px 6px", borderRadius: 6 };
|
|
1620
1608
|
}
|
|
1621
1609
|
|
|
1622
|
-
|
|
1623
|
-
|
|
1610
|
+
/** 注入的布局推挤样式(仅一次):右侧面板展开时把 #root 往左推,形成「集成侧边栏」,
|
|
1611
|
+
* 与 dsh-better-sidebar 右侧面板同一机制(margin + width calc)。 */
|
|
1612
|
+
const LAYOUT_CSS_ID = "source-code-mgmt-layout";
|
|
1613
|
+
let layoutCssInjected = false;
|
|
1614
|
+
function ensureLayoutCss() {
|
|
1615
|
+
if (layoutCssInjected) return;
|
|
1616
|
+
layoutCssInjected = true;
|
|
1617
|
+
const tag = document.createElement("style");
|
|
1618
|
+
tag.id = LAYOUT_CSS_ID;
|
|
1619
|
+
tag.setAttribute("data-source-code-mgmt-css", "");
|
|
1620
|
+
tag.textContent =
|
|
1621
|
+
// 推挤 #root:右侧面板占据布局而非浮在内容上方(VSCode 侧边栏手感),
|
|
1622
|
+
// 用 calc(100% - var) 避免桌面壳把 #root 设成 width:100% 时的加性溢出。
|
|
1623
|
+
"#root {" +
|
|
1624
|
+
" margin-right: var(--scm-push, 0px);" +
|
|
1625
|
+
" width: calc(100% - var(--scm-push, 0px));" +
|
|
1626
|
+
" transition: margin-right var(--ds-transition-duration-slow, .25s) var(--ds-ease-in-out, ease)," +
|
|
1627
|
+
" width var(--ds-transition-duration-slow, .25s) var(--ds-ease-in-out, ease);" +
|
|
1628
|
+
" }\n" +
|
|
1629
|
+
"body[data-source-code-mgmt-dragging] #root { transition: none; }\n" +
|
|
1630
|
+
"body[data-source-code-mgmt-dragging] { cursor: col-resize; user-select: none; }\n";
|
|
1631
|
+
document.head.appendChild(tag);
|
|
1624
1632
|
}
|
|
1625
1633
|
|
|
1626
|
-
/** 共享的「仓库 /
|
|
1634
|
+
/** 共享的「仓库 / 分支」小图标:既用于左轨按钮,也用作侧边栏 Tab 图标。 */
|
|
1627
1635
|
function repoIcon(size) {
|
|
1628
1636
|
const s = size || 18;
|
|
1629
1637
|
return h("svg", {
|
|
@@ -1637,47 +1645,101 @@ window.__ModuleLoader__.load({
|
|
|
1637
1645
|
);
|
|
1638
1646
|
}
|
|
1639
1647
|
|
|
1640
|
-
|
|
1641
|
-
|
|
1648
|
+
// ---------- branch B:右上角 header 入口 + 右侧集成面板 ----------
|
|
1649
|
+
// 未安装 dsh-better-sidebar 时使用:把「代码管理」按钮注册进 DSH 的
|
|
1650
|
+
// conversation.session.header.utilities 槽位(Session log 所在的右对齐列表),
|
|
1651
|
+
// 因此它天然出现在 Session log 旁、样式一致的胶囊按钮、间距 8px 不挤在一起;
|
|
1652
|
+
// 点击后在右侧展开一个 dsh-better-sidebar 外观的固定面板(推挤 #root),复用 ScmPanel。
|
|
1653
|
+
const SCM_PANEL_W = 620;
|
|
1654
|
+
|
|
1655
|
+
/** Session log 同款胶囊按钮样式(描边、圆角 18、透明底),放在 header utilities 列表里。 */
|
|
1656
|
+
function headerBtnStyle() {
|
|
1642
1657
|
return {
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
fontSize: 13, lineHeight: 1,
|
|
1648
|
-
|
|
1658
|
+
display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 4,
|
|
1659
|
+
height: 32, padding: "6px 12px",
|
|
1660
|
+
border: "1px solid " + T.border, borderRadius: 18,
|
|
1661
|
+
color: T.label, background: "transparent",
|
|
1662
|
+
font: "inherit", fontSize: 13, fontWeight: 400, lineHeight: 1,
|
|
1663
|
+
cursor: "pointer", whiteSpace: "nowrap",
|
|
1649
1664
|
};
|
|
1650
1665
|
}
|
|
1651
1666
|
|
|
1652
|
-
/**
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1667
|
+
/** 右侧面板的最小 / 最大宽度(拖拽改宽时钳制,防止拖得过窄或超出视口)。 */
|
|
1668
|
+
const SCM_PANEL_MIN = 360;
|
|
1669
|
+
const SCM_PANEL_MAX_FRAC = 0.92; // 拖拽上限:视口宽度的 92%
|
|
1670
|
+
|
|
1671
|
+
/** 右侧集成面板拖拽改宽:拖动面板左边缘调整宽度(像 dsh-better-sidebar)。要点:
|
|
1672
|
+
* 拖拽期间逐帧直接写 DOM(面板 style.width + --scm-push CSS 变量),避免每帧触发
|
|
1673
|
+
* React 重渲染造成卡顿;在 pointerup 时把最终宽度提交进 state,重开面板仍保持。 */
|
|
1674
|
+
function makeResizeHandler(getPanelEl, getW, onCommit) {
|
|
1675
|
+
return (e) => {
|
|
1676
|
+
e.preventDefault();
|
|
1677
|
+
const startX = e.clientX;
|
|
1678
|
+
const startW = getW();
|
|
1679
|
+
let lastW = startW;
|
|
1680
|
+
const body = document.body;
|
|
1681
|
+
body.setAttribute("data-source-code-mgmt-dragging", "");
|
|
1682
|
+
const onMove = (ev) => {
|
|
1683
|
+
// 面板贴右(right:0),往左拖 = 变宽;增量 = 起点X - 当前X。
|
|
1684
|
+
let w = startW + (startX - ev.clientX);
|
|
1685
|
+
const max = Math.round(window.innerWidth * SCM_PANEL_MAX_FRAC);
|
|
1686
|
+
w = Math.round(Math.max(SCM_PANEL_MIN, Math.min(max, w)));
|
|
1687
|
+
lastW = w;
|
|
1688
|
+
const el = getPanelEl();
|
|
1689
|
+
if (el) el.style.width = w + "px";
|
|
1690
|
+
document.documentElement.style.setProperty("--scm-push", w + "px");
|
|
1691
|
+
};
|
|
1692
|
+
const onUp = () => {
|
|
1693
|
+
body.removeAttribute("data-source-code-mgmt-dragging");
|
|
1694
|
+
window.removeEventListener("pointermove", onMove);
|
|
1695
|
+
window.removeEventListener("pointerup", onUp);
|
|
1696
|
+
if (lastW !== startW) onCommit(lastW);
|
|
1697
|
+
};
|
|
1698
|
+
window.addEventListener("pointermove", onMove);
|
|
1699
|
+
window.addEventListener("pointerup", onUp);
|
|
1661
1700
|
};
|
|
1662
1701
|
}
|
|
1663
1702
|
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
function ScmFloatingEntry() {
|
|
1703
|
+
/** 右上角「代码管理」按钮 + 点击后打开的右侧集成面板(dsh-better-sidebar 外观,
|
|
1704
|
+
* 可拖拽左边缘调整宽度)。 */
|
|
1705
|
+
function HeaderScmAction() {
|
|
1668
1706
|
const [open, setOpen] = useState(false);
|
|
1707
|
+
const [panelW, setPanelW] = useState(SCM_PANEL_W);
|
|
1708
|
+
const panelRef = useRef(null);
|
|
1709
|
+
useEffect(() => { ensureLayoutCss(); }, []);
|
|
1710
|
+
// 右侧面板打开时推挤 #root(margin-right),关闭时归零。宽度跟随 panelW。
|
|
1711
|
+
useEffect(() => {
|
|
1712
|
+
document.documentElement.style.setProperty("--scm-push", open ? panelW + "px" : "0px");
|
|
1713
|
+
return () => { document.documentElement.style.removeProperty("--scm-push"); };
|
|
1714
|
+
}, [open, panelW]);
|
|
1715
|
+
// 拖拽改宽处理器(面板左边缘的拖拽条)。
|
|
1716
|
+
const onResize = makeResizeHandler(
|
|
1717
|
+
() => panelRef.current,
|
|
1718
|
+
() => panelW,
|
|
1719
|
+
(w) => setPanelW(w),
|
|
1720
|
+
);
|
|
1669
1721
|
return h(React.Fragment, null,
|
|
1670
|
-
h("button", {
|
|
1671
|
-
|
|
1672
|
-
style: floatBtnStyle(),
|
|
1673
|
-
title: "源代码管理",
|
|
1674
|
-
"aria-label": "源代码管理",
|
|
1675
|
-
onClick: () => setOpen(true),
|
|
1676
|
-
}, repoIcon(16), h("span", { style: { fontSize: 13 } }, "代码管理")),
|
|
1722
|
+
h("button", { type: "button", style: headerBtnStyle(), title: "源代码管理", "aria-label": "源代码管理", onClick: () => setOpen(true) },
|
|
1723
|
+
repoIcon(14), h("span", { style: { fontSize: 13 } }, "代码管理")),
|
|
1677
1724
|
open ? ReactDOM.createPortal(
|
|
1678
|
-
h("div", { style: { position: "fixed", inset: 0, zIndex: 1000 }, role: "presentation" },
|
|
1679
|
-
h("div", {
|
|
1680
|
-
|
|
1725
|
+
h("div", { style: { position: "fixed", inset: 0, zIndex: 1000, pointerEvents: "none" }, role: "presentation" },
|
|
1726
|
+
h("div", { ref: panelRef, style: {
|
|
1727
|
+
position: "absolute", top: 0, right: 0, bottom: 0, width: panelW + "px",
|
|
1728
|
+
boxSizing: "border-box", overflow: "visible", pointerEvents: "auto",
|
|
1729
|
+
background: "var(--dsw-alias-bg-layer-1, rgba(128,128,128,.08))",
|
|
1730
|
+
borderLeft: "1px solid " + T.border,
|
|
1731
|
+
boxShadow: "var(--dsw-overlay-shadow, 0 12px 32px rgba(0,0,0,.35))",
|
|
1732
|
+
} },
|
|
1733
|
+
// 左边缘拖拽条(改宽)
|
|
1734
|
+
h("div", {
|
|
1735
|
+
onPointerDown: onResize,
|
|
1736
|
+
title: "拖动调整面板宽度",
|
|
1737
|
+
"aria-label": "拖动调整面板宽度",
|
|
1738
|
+
style: {
|
|
1739
|
+
position: "absolute", top: 0, bottom: 0, left: -3, width: 7,
|
|
1740
|
+
cursor: "col-resize", pointerEvents: "auto", zIndex: 1,
|
|
1741
|
+
},
|
|
1742
|
+
}),
|
|
1681
1743
|
h(ScmPanel, { variant: "drawer", onClose: () => setOpen(false) })
|
|
1682
1744
|
)
|
|
1683
1745
|
),
|
|
@@ -1737,28 +1799,40 @@ window.__ModuleLoader__.load({
|
|
|
1737
1799
|
single: true,
|
|
1738
1800
|
component: () => h(ScmPanel, { variant: "tab" }),
|
|
1739
1801
|
}));
|
|
1740
|
-
// 若此前已挂载了分支 B
|
|
1802
|
+
// 若此前已挂载了分支 B 的 header 入口(降级路径),立即拆除它。
|
|
1741
1803
|
if (entryUnmount) { entryUnmount(); entryUnmount = null; }
|
|
1742
1804
|
return true;
|
|
1743
1805
|
};
|
|
1744
1806
|
|
|
1745
1807
|
if (tryRegisterTab()) return;
|
|
1746
1808
|
|
|
1747
|
-
// 分支 B:此刻未检测到 dsh-better-sidebar ——
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
};
|
|
1758
|
-
|
|
1759
|
-
|
|
1809
|
+
// 分支 B:此刻未检测到 dsh-better-sidebar —— 把「代码管理」按钮注册进 DSH 的
|
|
1810
|
+
// conversation.session.header.utilities 槽位(Session log 所在的右对齐列表),
|
|
1811
|
+
// 因此它天然出现在 Session log 旁、间距 8px 不挤在一起;点击后 HeaderScmAction
|
|
1812
|
+
// 打开右侧 dsh-better-sidebar 外观的集成面板(推挤 #root)。
|
|
1813
|
+
const slots = typeof ctx.get === "function" ? ctx.get("slots") : undefined;
|
|
1814
|
+
if (slots && typeof slots.inject === "function") {
|
|
1815
|
+
ctx.effect(() => slots.inject('conversation.session.header.utilities', () => slots.register({
|
|
1816
|
+
name: 'conversation.session.header.utilities',
|
|
1817
|
+
id: 'source-code-mgmt',
|
|
1818
|
+
order: 200,
|
|
1819
|
+
}, HeaderScmAction)), 'source-code-mgmt: header utility');
|
|
1820
|
+
} else if (ReactDOM) {
|
|
1821
|
+
// slots 服务不可用(极少数环境)——降级:右上角浮动按钮 + 右侧面板。
|
|
1822
|
+
ctx.effect(() => {
|
|
1823
|
+
const hostEl = document.createElement("div");
|
|
1824
|
+
hostEl.setAttribute("data-source-code-mgmt-entry", "");
|
|
1825
|
+
document.body.appendChild(hostEl);
|
|
1826
|
+
const root = mountClientRoot(hostEl, h(HeaderScmAction));
|
|
1827
|
+
entryUnmount = () => {
|
|
1828
|
+
try { if (root && typeof root.unmount === "function") root.unmount(); } catch {}
|
|
1829
|
+
hostEl.remove();
|
|
1830
|
+
};
|
|
1831
|
+
return () => { if (entryUnmount) { entryUnmount(); entryUnmount = null; } };
|
|
1832
|
+
});
|
|
1833
|
+
}
|
|
1760
1834
|
|
|
1761
|
-
// 兜底重试一次:better-sidebar 若在本插件之后激活,延迟补注册 Tab
|
|
1835
|
+
// 兜底重试一次:better-sidebar 若在本插件之后激活,延迟补注册 Tab 并拆除 header 入口。
|
|
1762
1836
|
// 单次 setTimeout,无轮询、零 I/O,几乎不耗资源;插件卸载时清除。
|
|
1763
1837
|
const retryTimer = window.setTimeout(() => { tryRegisterTab(); }, 1500);
|
|
1764
1838
|
ctx.effect(() => () => window.clearTimeout(retryTimer));
|
package/lib/index.js
CHANGED
|
@@ -1402,6 +1402,62 @@ function commitFlow(dir, message, paths) {
|
|
|
1402
1402
|
}
|
|
1403
1403
|
}
|
|
1404
1404
|
|
|
1405
|
+
/**
|
|
1406
|
+
* Push STAGED changes only: commit whatever is currently staged (using the
|
|
1407
|
+
* given message, or an autogenerated one when empty), then push to origin.
|
|
1408
|
+
* Unlike {@link pushFlow} it does NOT go on to stage untracked/unstaged files
|
|
1409
|
+
* itself and does NOT overwrite the user's own commit message. When nothing is
|
|
1410
|
+
* newly staged yet (e.g. the local branch already has commits ahead), it simply
|
|
1411
|
+
* pushes those existing commits.
|
|
1412
|
+
* @param {string} dir - local folder.
|
|
1413
|
+
* @param {string} [message] - commit message used only when something is staged
|
|
1414
|
+
* and the caller supplies one; empty falls back to an autogenerated message.
|
|
1415
|
+
*/
|
|
1416
|
+
function pushStagedFlow(dir, message) {
|
|
1417
|
+
const guard = requireGitRepo(dir)
|
|
1418
|
+
if (guard) return { ok: false, error: guard.error }
|
|
1419
|
+
|
|
1420
|
+
// Remote must exist before we can push.
|
|
1421
|
+
const hasRemote = run(GIT, ['-C', dir, 'remote', 'get-url', 'origin']).ok
|
|
1422
|
+
if (!hasRemote) {
|
|
1423
|
+
return { ok: false, needsRemote: true, error: '尚未配置远程仓库 origin,请使用「新建仓库」创建远程仓库。' }
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
const msg = String(message || '').trim()
|
|
1427
|
+
let committed = false
|
|
1428
|
+
let commitHash
|
|
1429
|
+
// Commit staged content only if there is anything staged right now.
|
|
1430
|
+
const hasStaged = run(GIT, ['-C', dir, 'diff', '--cached', '--quiet']).code !== 0
|
|
1431
|
+
if (hasStaged) {
|
|
1432
|
+
const ident = run(GIT, ['-C', dir, 'config', 'user.email'])
|
|
1433
|
+
if (!ident.ok) {
|
|
1434
|
+
run(GIT, ['-C', dir, 'config', 'user.name', 'DSH User'])
|
|
1435
|
+
run(GIT, ['-C', dir, 'config', 'user.email', 'dsh@localhost'])
|
|
1436
|
+
}
|
|
1437
|
+
const useMsg = msg || ('chore: update ' + baseName(dir))
|
|
1438
|
+
const commit = run(GIT, ['-C', dir, 'commit', '-m', useMsg])
|
|
1439
|
+
if (!commit.ok) return { ok: false, error: (commit.stderr || commit.stdout).trim() || 'git commit 失败' }
|
|
1440
|
+
committed = true
|
|
1441
|
+
const rev = run(GIT, ['-C', dir, 'rev-parse', '--short', 'HEAD'])
|
|
1442
|
+
commitHash = rev.ok ? rev.stdout.trim() : undefined
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
const curBranch = run(GIT, ['-C', dir, 'rev-parse', '--abbrev-ref', 'HEAD'])
|
|
1446
|
+
const branchName = curBranch.ok ? curBranch.stdout.trim() : 'main'
|
|
1447
|
+
const push = run(GIT, ['-C', dir, 'push', '-u', 'origin', branchName], { timeout: 120_000 })
|
|
1448
|
+
const pushed = push.ok
|
|
1449
|
+
return {
|
|
1450
|
+
ok: pushed,
|
|
1451
|
+
needsRemote: false,
|
|
1452
|
+
committed,
|
|
1453
|
+
commitHash,
|
|
1454
|
+
message: msg || (committed ? ('chore: update ' + baseName(dir)) : undefined),
|
|
1455
|
+
branch: branchName,
|
|
1456
|
+
pushed,
|
|
1457
|
+
pushError: pushed ? undefined : (push.stderr || push.stdout).trim(),
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1405
1461
|
/**
|
|
1406
1462
|
* List branches (current first) using `git for-each-ref`.
|
|
1407
1463
|
* @param {string} dir - local folder.
|
|
@@ -2099,6 +2155,13 @@ export function apply(ctx) {
|
|
|
2099
2155
|
return pushFlow(body.dir || fallbackDir)
|
|
2100
2156
|
})
|
|
2101
2157
|
},
|
|
2158
|
+
'/push-staged': async (req, res) => {
|
|
2159
|
+
if (req.method !== 'POST') return methodNotAllowed(res, req.method)
|
|
2160
|
+
await handle(req, res, async (req) => {
|
|
2161
|
+
const body = JSON.parse((await readBody(req)) || '{}')
|
|
2162
|
+
return pushStagedFlow(body.dir || fallbackDir, body.message)
|
|
2163
|
+
})
|
|
2164
|
+
},
|
|
2102
2165
|
'/pull': async (req, res) => {
|
|
2103
2166
|
if (req.method !== 'POST') return methodNotAllowed(res, req.method)
|
|
2104
2167
|
await handle(req, res, async (req) => {
|
package/package.json
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "source-code-mgmt",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "DSH plugin: source code management. Registers「代码管理」as a dsh-better-sidebar sidebar Tab when installed, otherwise a
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "lib/index.js",
|
|
7
|
-
"exports": {
|
|
8
|
-
".": {
|
|
9
|
-
"default": "./lib/index.js"
|
|
10
|
-
},
|
|
11
|
-
"./client": {
|
|
12
|
-
"default": "./lib/client.js"
|
|
13
|
-
},
|
|
14
|
-
"./package.json": "./package.json"
|
|
15
|
-
},
|
|
16
|
-
"dsh": {
|
|
17
|
-
"bundle": "./lib/index.js",
|
|
18
|
-
"client": {
|
|
19
|
-
"inject": [
|
|
20
|
-
"@deepseek-ai/dsh-client-runtime"
|
|
21
|
-
],
|
|
22
|
-
"platform": "web"
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
"peerDependencies": {
|
|
26
|
-
"react": "^18.2.0",
|
|
27
|
-
"dsh-better-sidebar": ">=0.4.0"
|
|
28
|
-
},
|
|
29
|
-
"peerDependenciesMeta": {
|
|
30
|
-
"dsh-better-sidebar": {
|
|
31
|
-
"optional": true
|
|
32
|
-
}
|
|
33
|
-
},
|
|
34
|
-
"files": [
|
|
35
|
-
"lib/**/*.js"
|
|
36
|
-
],
|
|
37
|
-
"license": "MIT"
|
|
38
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "source-code-mgmt",
|
|
3
|
+
"version": "1.8.0",
|
|
4
|
+
"description": "DSH plugin: source code management. Registers「代码管理」as a dsh-better-sidebar sidebar Tab when installed, otherwise a button in the DSH session header's right-aligned utilities (beside \"Session log\") opening a right-side integrated panel that pushes the main content — env checks (git/gh), SSH ed25519 key setup, GitHub/Gitee dual-platform (SSH config 443 + Gitee OpenAPI), connectivity test, push / new repo, and auto-ignore files over 100MB with per-file skipped reasons. Cross-platform (Windows / Linux / macOS).",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "lib/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"default": "./lib/index.js"
|
|
10
|
+
},
|
|
11
|
+
"./client": {
|
|
12
|
+
"default": "./lib/client.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"dsh": {
|
|
17
|
+
"bundle": "./lib/index.js",
|
|
18
|
+
"client": {
|
|
19
|
+
"inject": [
|
|
20
|
+
"@deepseek-ai/dsh-client-runtime"
|
|
21
|
+
],
|
|
22
|
+
"platform": "web"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"peerDependencies": {
|
|
26
|
+
"react": "^18.2.0",
|
|
27
|
+
"dsh-better-sidebar": ">=0.4.0"
|
|
28
|
+
},
|
|
29
|
+
"peerDependenciesMeta": {
|
|
30
|
+
"dsh-better-sidebar": {
|
|
31
|
+
"optional": true
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"lib/**/*.js"
|
|
36
|
+
],
|
|
37
|
+
"license": "MIT"
|
|
38
|
+
}
|