source-code-mgmt 1.10.0 → 1.11.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.en.md +229 -210
- package/README.md +27 -6
- package/cordis.patch.yml +15 -15
- package/lib/client.js +120 -26
- package/lib/index.js +141 -13
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -1,210 +1,229 @@
|
|
|
1
|
-
# source-code-mgmt — DSH Source Code Management Plugin
|
|
2
|
-
|
|
3
|
-
> Version: **v1.
|
|
4
|
-
|
|
5
|
-
> A source-code management plugin for the DSH Web GUI: it bundles「environment check → SSH setup → commit/push/upload code」into one「Code Management」panel with GitHub / Gitee dual-platform support.
|
|
6
|
-
|
|
7
|
-
> **Bilingual UI, live**: the panel and host-side messages follow DSH's language setting (Settings → General → Language) — switching between 中文 and English takes effect instantly, no refresh or restart needed.
|
|
8
|
-
|
|
9
|
-
> The entry point adapts automatically: when [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) is installed,「Code Management」appears as a new sidebar **Tab** of that sidebar; otherwise a「Code Management」button is registered in the **right-aligned header list beside "Session log"** (same pill style, 8px gap), opening a right-side integrated panel that pushes the main content. Both forms share the same panel UI and no longer use a left-rail bottom button.
|
|
10
|
-
|
|
11
|
-
> **UI language:** the panel follows DSH's language setting (Settings → General → Language), live — Chinese (中文) or English, including all host-side messages. No restart needed when you switch.
|
|
12
|
-
|
|
13
|
-
## Features
|
|
14
|
-
|
|
15
|
-
Entry points (auto-detected, no manual switching):
|
|
16
|
-
|
|
17
|
-
- **dsh-better-sidebar installed:**「Code Management」registers as a new **Tab page** of its sidebar;
|
|
18
|
-
- **dsh-better-sidebar not installed:** a「Code Management」button in the **right-aligned header list beside "Session log"** (registered through DSH's `conversation.session.header.utilities` slot), opening a right-side integrated panel that pushes the main content left.
|
|
19
|
-
|
|
20
|
-
> Detection is a single in-memory read at activation time (`ctx.get('betterSidebar')`) — zero I/O, zero network, no impact on DSH startup.
|
|
21
|
-
|
|
22
|
-
The panel has three steps:
|
|
23
|
-
|
|
24
|
-
### ① Environment Check
|
|
25
|
-
- Shows the **OS** (friendly names `Windows` / `macOS` / `Linux`, from the underlying `win32` / `darwin` / `linux` platform ids)
|
|
26
|
-
- Detects **Git** and **GitHub CLI** presence and versions (e.g. `git version 2.55.0`, `gh version 2.97.0`)
|
|
27
|
-
- Detects whether an **SSH** client is available
|
|
28
|
-
- **Missing tools → install guidance + one-click install**: when a tool is missing, the row shows「❌ 未安装」+「复制安装命令」(copy install command) +「安装」(install) — install picks the package manager automatically (winget / built-in features on Windows, brew on macOS, apt/dnf/pacman on Linux, may need admin rights) and re-detects afterwards
|
|
29
|
-
|
|
30
|
-
### ② SSH Key & Connectivity
|
|
31
|
-
- **Platform selector**: GitHub (default) / Gitee — decides the SSH config target and connectivity test below
|
|
32
|
-
- **Auto-detect ed25519 key**: scans `~/.ssh/*.pub` for existing ed25519 public keys — prefers `id_ed25519`, otherwise the first found (any name, e.g. `github_ed25519`); falls back to `id_ed25519` when none exist. The status line shows the actual key filename, and the SSH config `IdentityFile` uses it too
|
|
33
|
-
- One-click **generate ed25519 key** (no passphrase; reuses an existing ed25519 key instead of duplicating)
|
|
34
|
-
- One-click **write SSH config** (GitHub: `github.com → ssh.github.com:443`; Gitee: `gitee.com` port 443) — see [Do I need the 443 config?](#do-i-need-the-443-config) below
|
|
35
|
-
- **Test connection** `ssh -T git@github.com` (GitHub) or `ssh -T git@gitee.com` (Gitee)
|
|
36
|
-
- Shows the public key content for easy copy-upload to the platform
|
|
37
|
-
- Detects whether `gh` is logged in and which account
|
|
38
|
-
|
|
39
|
-
### ③ Code Management
|
|
40
|
-
- **Follows ②'s platform**: all「detect / create / visibility」logic switches with the platform selector (GitHub via `gh` CLI, Gitee via Gitee OpenAPI)
|
|
41
|
-
- **Gitee token** (Gitee mode only): enter a Gitee personal access token (needs `projects` permission) → stored at `~/.dsh/storages/source-code-mgmt-gitee.json` on the machine (0600, **not inside the plugin dir**, never echoed to the browser/logs); one-click clear; invalid tokens are removed automatically
|
|
42
|
-
- **Workspace selector**: dropdown of DSH-registered workspace folders
|
|
43
|
-
- **Select folder →**: paste an absolute path or click「Browse…」for a native folder picker; confirmed folders are **persisted into a custom folder list** (`~/.dsh/storages/source-code-mgmt-dirs.json`, separate from the plugin dir — no personal paths leak), shown with a custom-folder badge and a ✕ to remove the entry (record only, never deletes the actual folder)
|
|
44
|
-
- Shows repo status: platform source, branch, remote, pending change count, ahead/behind remote, >100MB files
|
|
45
|
-
- **View details**: when there are changes, a「查看」(view) button opens a dialog listing changed/added/deleted/renamed **files or folders
|
|
46
|
-
- **Local Git workflow** (does not change the remote-sync logic):
|
|
47
|
-
- **Stage / unstage** per file in the changes dialog (distinguishing staged/unstaged by `git status` XY codes), with「已暂存 / 未暂存」markers
|
|
48
|
-
- **Commit message input + Commit button** above the repo name (git repos with changes only) — custom message, or auto-generated when left empty
|
|
49
|
-
- **Branch「Switch」** button — dialog listing branches, click to `checkout`
|
|
50
|
-
- **History button** — dialog listing commits (hash + subject + author + date), each with view (side-by-side diff), **revert**, **cherry-pick** (both with confirmation, as they rewrite history)
|
|
51
|
-
- **Remote matching by platform + current account**: ③'s「remote / sync」only counts remotes of the current platform **whose owner equals the currently logged-in account** (GitHub platform = `gh` account, Gitee platform = Gitee token account). So switching to Gitee never reads the GitHub origin; someone else's / another org's repos (e.g. `deepseek-ai/deepseek-harness`) are never treated as "your own remote", and ahead/behind is not computed for them. With no account-owned remote, the panel shows「(无)」and offers the same-name repo check + create-and-push flow
|
|
52
|
-
- **Init Git**: shown when the folder is **not** a git repo but a same-name repo already exists remotely — runs `git init` only (+ default identity), **no pull, no push**, you choose the next step
|
|
53
|
-
- **Create repo & push**: repo name defaults to the **folder name** (read-only), optional private/public; the button is disabled with a hint when a same-name repo already exists. For a brand-new directory with no commits, it auto-stages an initial commit before creating the repo to avoid "no commits found"
|
|
54
|
-
- GitHub: `gh repo create --private|--public --source=. --push`
|
|
55
|
-
- Gitee: Gitee OpenAPI `POST /user/repos` to create, then sets the SSH remote `git@gitee.com:<owner>/<name>.git` and `git push` (through ②'s SSH key)
|
|
56
|
-
|
|
57
|
-
### Data loading timing (fetch on open, "refreshing…" indicator)
|
|
58
|
-
DSH does **not** sync repos on startup — it only prefetches static env/SSH/workspace lists. Opening the plugin, switching workspace/folder, refreshing, and any push/pull/stage/commit operation fetch the latest repo status over the network and show a「⟳ 刷新中…」indicator — no stale data (like an old "no changes") when opening/reopening/switching.
|
|
59
|
-
|
|
60
|
-
## Do I need the 443 config?
|
|
61
|
-
|
|
62
|
-
Short answer for users outside mainland China / port-22-blocked networks: **usually no.**
|
|
63
|
-
|
|
64
|
-
- GitHub's standard SSH endpoint is `git@github.com` on **port 22**, and it works out of the box on almost every network outside mainland China.
|
|
65
|
-
- The plugin's「write SSH config」step writes GitHub's **officially supported** port-443 fallback (`Host github.com → HostName ssh.github.com, Port 443`). It exists for networks that block port 22 (common in mainland China, plus some corporate/school/campus or otherwise firewalled networks).
|
|
66
|
-
- The step is **fully optional and user-initiated** — you can simply skip it: generate the key → add the public key to GitHub → test connection → push. Everything runs over port 22.
|
|
67
|
-
- Writing it anyway is harmless (GitHub officially supports SSH over 443; the only edge case is networks that block 443 as well). Gitee is a China-hosted platform, so it's only relevant if you actually use Gitee.
|
|
68
|
-
|
|
69
|
-
## Installation
|
|
70
|
-
|
|
71
|
-
> This plugin ships as a **Profile Bundle**: its `package.json` declares `dsh.bundle` (carrying a `cordis.patch.yml` config layer), so `dsh plugin --profile web add` **installs and activates it in one step** — no manual config editing.
|
|
72
|
-
|
|
73
|
-
### Option 1: npm package (recommended)
|
|
74
|
-
|
|
75
|
-
Run this from **any directory** (the command locates/initializes the `web` profile itself):
|
|
76
|
-
|
|
77
|
-
```bash
|
|
78
|
-
dsh plugin --profile web add source-code-mgmt
|
|
79
|
-
```
|
|
80
|
-
|
|
81
|
-
> The command runs `pnpm add` in the web profile directory, then reconciles the plugin layer: because this package declares `dsh.bundle`, it is automatically appended to `dsh.profile.bundles` (see `~/.dsh/profiles/web/package.json`) and registered into the Cordis loader tree — **one command, done**.
|
|
82
|
-
|
|
83
|
-
After installing, **fully restart dsh web** (stop the old process — not a page refresh), then **F5** in the browser. The「Code Management」entry appears (sidebar Tab with dsh-better-sidebar, otherwise the header button + right panel).
|
|
84
|
-
|
|
85
|
-
### Option 2: local directory (development / testing)
|
|
86
|
-
|
|
87
|
-
**Windows (PowerShell):**
|
|
88
|
-
```powershell
|
|
89
|
-
# install local source (link: protocol — a symlink, source edits take effect immediately)
|
|
90
|
-
dsh plugin --profile web add link:C:/path/to/source-code-mgmt
|
|
91
|
-
dsh web
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
**Linux / macOS:**
|
|
95
|
-
```bash
|
|
96
|
-
dsh plugin --profile web add link:/home/yourname/path/to/source-code-mgmt
|
|
97
|
-
dsh web
|
|
98
|
-
```
|
|
99
|
-
|
|
100
|
-
### Option 3: from GitHub (distribution)
|
|
101
|
-
|
|
102
|
-
```bash
|
|
103
|
-
dsh plugin --profile web add git+https://github.com/Zhucy123/source-code-mgmt.git
|
|
104
|
-
dsh web
|
|
105
|
-
```
|
|
106
|
-
|
|
107
|
-
> A git install **copies** the source into node_modules — to pick up source changes, re-run `dsh plugin --profile web add ...` (unlike `link:`, which is a symlink).
|
|
108
|
-
|
|
109
|
-
### Verifying the install
|
|
110
|
-
|
|
111
|
-
After installing and restarting:
|
|
112
|
-
|
|
113
|
-
1. **Dependency written**: `source-code-mgmt` is in `dependencies` of `~/.dsh/profiles/web/package.json`.
|
|
114
|
-
2. **Added to the config layer**: `source-code-mgmt` is in the `dsh.profile.bundles` list of the same file (written automatically by `dsh plugin add` — no manual editing).
|
|
115
|
-
3. **Symlink created (`link:` only)**: `~/.dsh/profiles/web/node_modules/source-code-mgmt` points at your source dir (a Junction on Windows).
|
|
116
|
-
4. **Entry visible after restart**: sidebar「Code Management」Tab with dsh-better-sidebar, otherwise the header button beside "Session log" opening the right panel.
|
|
117
|
-
|
|
118
|
-
### Troubleshooting
|
|
119
|
-
|
|
120
|
-
| Symptom | Cause / fix |
|
|
121
|
-
|---------|-------------|
|
|
122
|
-
| Installed with `dsh plugin add` and restarted, but no button | Most common: the process was **refreshed, not fully restarted**. Stop the old `dsh web` process (it may still hold port 3080) and start it again. |
|
|
123
|
-
| Install prints「declares no dsh.bundle」 | The installed version lacks the bundle declaration (old version or a package missing `cordis.patch.yml`). Reinstall/update with version ≥ 1.9.0. |
|
|
124
|
-
|「Failed to load plugins」 | The host-side `lib/index.js` failed to boot (usually a dependency resolution issue). Check the startup log and confirm `node_modules` dependencies are complete. |
|
|
125
|
-
|
|
126
|
-
## Usage
|
|
127
|
-
|
|
128
|
-
1. Restart dsh web and refresh the browser.
|
|
129
|
-
2. Click the「**Code Management**」entry (sidebar Tab with dsh-better-sidebar, otherwise the header button beside "Session log").
|
|
130
|
-
3. ① Confirm Git / GitHub CLI are installed → ② generate a key and test connectivity → ③ pick a workspace, then push or create a new repo.
|
|
131
|
-
|
|
132
|
-
## Backend API routes
|
|
133
|
-
|
|
134
|
-
| Route | Method | Description |
|
|
135
|
-
|-------|--------|-------------|
|
|
136
|
-
| `/api/source-code-mgmt/env` | GET | Environment check (git/gh versions) |
|
|
137
|
-
| `/api/source-code-mgmt/install-tool` | POST | One-click install of a missing tool (body `tool`: `git`/`gh`/`ssh`, package manager picked per platform) |
|
|
138
|
-
| `/api/source-code-mgmt/ssh` | GET | SSH key / config / gh login status |
|
|
139
|
-
| `/api/source-code-mgmt/gen-key` | POST | Generate an ed25519 key |
|
|
140
|
-
| `/api/source-code-mgmt/write-config` | POST | Write SSH config (body `provider`: `github` default / `gitee`) |
|
|
141
|
-
| `/api/source-code-mgmt/ssh-test` | POST | Test SSH connectivity (body `provider`: `github` default / `gitee`) |
|
|
142
|
-
| `/api/source-code-mgmt/default-dir` | GET | Current workspace folder |
|
|
143
|
-
| `/api/source-code-mgmt/workspaces` | GET | List all workspace folders + custom folder set |
|
|
144
|
-
| `/api/source-code-mgmt/pick-dir` | POST | Native folder picker on the host; returns the chosen path |
|
|
145
|
-
| `/api/source-code-mgmt/add-workspace` | POST | Validate a folder and persist it to the custom list; returns the merged workspace list |
|
|
146
|
-
| `/api/source-code-mgmt/remove-workspace` | POST | Remove a custom-folder dropdown record only (never deletes the folder) |
|
|
147
|
-
| `/api/source-code-mgmt/align` | POST | Hard align: `git fetch` + `git reset --hard origin/<branch>` (full reset to remote) |
|
|
148
|
-
| `/api/source-code-mgmt/init-git` | POST | `git init` + default identity only — no pull/push |
|
|
149
|
-
| `/api/source-code-mgmt/repo-exists` | POST | Check whether a same-name repo exists (body `provider`: `github`/`gitee`) |
|
|
150
|
-
| `/api/source-code-mgmt/repo?dir=` | GET | Repo status (query `provider`: `github`/`gitee`) |
|
|
151
|
-
| `/api/source-code-mgmt/repo-diff?dir=&path=` | GET | Unified diff text for one changed file, on demand |
|
|
152
|
-
| `/api/source-code-mgmt/stage` | POST | Stage changes (body `dir`, `path`; empty `path` = all) |
|
|
153
|
-
| `/api/source-code-mgmt/unstage` | POST | Unstage changes (body `dir`, `path`; empty `path` = all) |
|
|
154
|
-
| `/api/source-code-mgmt/commit` | POST | Commit with a custom message (body `dir`, `message`, `paths?`) |
|
|
155
|
-
| `/api/source-code-mgmt/branches` | POST | List branches (current first) |
|
|
156
|
-
| `/api/source-code-mgmt/checkout` | POST | Switch branch (body `dir`, `branch`) |
|
|
157
|
-
| `/api/source-code-mgmt/log` | POST | Recent commit history (body `dir`, `count?`; returns hash/subject/author/date) |
|
|
158
|
-
| `/api/source-code-mgmt/revert` | POST | Revert a commit (body `dir`, `hash`) |
|
|
159
|
-
| `/api/source-code-mgmt/cherrypick` | POST | Cherry-pick a commit (body `dir`, `hash`) |
|
|
160
|
-
| `/api/source-code-mgmt/commit-diff` | POST | Full patch of a commit (body `dir`, `hash`) |
|
|
161
|
-
| `/api/source-code-mgmt/push` | POST | Commit and push (git operation, platform-agnostic) |
|
|
162
|
-
| `/api/source-code-mgmt/push-staged` | POST | Commit only staged content with your message, then push |
|
|
163
|
-
| `/api/source-code-mgmt/pull` | POST | `git pull --ff-only` (up-to-date / success / conflict feedback) |
|
|
164
|
-
| `/api/source-code-mgmt/merge-push` | POST | Pull and push (`git pull --rebase` + `git push`) |
|
|
165
|
-
| `/api/source-code-mgmt/force-push` | POST | Force push (`git push --force`) |
|
|
166
|
-
| `/api/source-code-mgmt/force-pull` | POST | Force pull (`git pull --force`) |
|
|
167
|
-
| `/api/source-code-mgmt/create` | POST | Create a repo and push (body `provider`; GitHub via `gh repo create`, Gitee via OpenAPI + SSH push) |
|
|
168
|
-
| `/api/source-code-mgmt/set-visibility` | POST | Change repo visibility (body `provider`; GitHub via `gh repo edit`, Gitee via `PATCH /repos/{owner}/{repo}`) |
|
|
169
|
-
| `/api/source-code-mgmt/gitee-token` | GET/POST | GET: token configured? + account; POST: save (`{token}`) or clear (`{clear:true}`) the Gitee token |
|
|
170
|
-
|
|
171
|
-
## Security
|
|
172
|
-
|
|
173
|
-
All routes are **loopback-only** (`sec-fetch-site` + Origin checks) — only the local machine's browser can call them; LAN/mobile sources get a 403, same policy as the control panel.
|
|
174
|
-
|
|
175
|
-
## Cross-platform
|
|
176
|
-
|
|
177
|
-
- **Windows / Linux / macOS**
|
|
178
|
-
- Platform detected via `process.platform`; `~/.ssh` resolved via `homedir()` (Windows: `C:\Users\<user>\.ssh`, Linux/macOS: `/home/<user>/.ssh` or `/Users/<user>/.ssh`)
|
|
179
|
-
- SSH config gets 0600 permissions on Linux/macOS
|
|
180
|
-
- **Automatic git / gh / ssh / ssh-keygen binary resolution** at startup: ① env overrides → ② PATH lookup (`.exe` added on Windows) → ③ (Windows only) Git's bundled dirs (`usr\bin` / `bin`), with the bare command name as a last resort. Git alone is enough even when `ssh` isn't on PATH — no per-machine config
|
|
181
|
-
- Optional explicit binary paths via env vars: `DSH_SCM_GIT` / `DSH_SCM_GH` / `DSH_SCM_SSH` / `DSH_SCM_SSH_KEYGEN`
|
|
182
|
-
- **SSH transport fix**: Git for Windows' bundled MSYS `ssh.exe` (`usr\bin\ssh.exe`) can fail with `couldn't create signal pipe, Win32 error 5` when spawned from a detached/agent process, breaking `git push`/`git pull`. The plugin injects `GIT_SSH` pointing at a working `ssh` (usually the system OpenSSH `C:\Windows\System32\OpenSSH\ssh.exe`) for git remote operations
|
|
183
|
-
- **
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
- Changes to `lib/
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
- **
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
1
|
+
# source-code-mgmt — DSH Source Code Management Plugin
|
|
2
|
+
|
|
3
|
+
> Version: **v1.11.0** | 中文版见 [README.md](README.md)
|
|
4
|
+
|
|
5
|
+
> A source-code management plugin for the DSH Web GUI: it bundles「environment check → SSH setup → commit/push/upload code」into one「Code Management」panel with GitHub / Gitee dual-platform support.
|
|
6
|
+
|
|
7
|
+
> **Bilingual UI, live**: the panel and host-side messages follow DSH's language setting (Settings → General → Language) — switching between 中文 and English takes effect instantly, no refresh or restart needed.
|
|
8
|
+
|
|
9
|
+
> The entry point adapts automatically: when [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) is installed,「Code Management」appears as a new sidebar **Tab** of that sidebar; otherwise a「Code Management」button is registered in the **right-aligned header list beside "Session log"** (same pill style, 8px gap), opening a right-side integrated panel that pushes the main content. Both forms share the same panel UI and no longer use a left-rail bottom button.
|
|
10
|
+
|
|
11
|
+
> **UI language:** the panel follows DSH's language setting (Settings → General → Language), live — Chinese (中文) or English, including all host-side messages. No restart needed when you switch.
|
|
12
|
+
|
|
13
|
+
## Features
|
|
14
|
+
|
|
15
|
+
Entry points (auto-detected, no manual switching):
|
|
16
|
+
|
|
17
|
+
- **dsh-better-sidebar installed:**「Code Management」registers as a new **Tab page** of its sidebar;
|
|
18
|
+
- **dsh-better-sidebar not installed:** a「Code Management」button in the **right-aligned header list beside "Session log"** (registered through DSH's `conversation.session.header.utilities` slot), opening a right-side integrated panel that pushes the main content left.
|
|
19
|
+
|
|
20
|
+
> Detection is a single in-memory read at activation time (`ctx.get('betterSidebar')`) — zero I/O, zero network, no impact on DSH startup.
|
|
21
|
+
|
|
22
|
+
The panel has three steps:
|
|
23
|
+
|
|
24
|
+
### ① Environment Check
|
|
25
|
+
- Shows the **OS** (friendly names `Windows` / `macOS` / `Linux`, from the underlying `win32` / `darwin` / `linux` platform ids)
|
|
26
|
+
- Detects **Git** and **GitHub CLI** presence and versions (e.g. `git version 2.55.0`, `gh version 2.97.0`)
|
|
27
|
+
- Detects whether an **SSH** client is available
|
|
28
|
+
- **Missing tools → install guidance + one-click install**: when a tool is missing, the row shows「❌ 未安装」+「复制安装命令」(copy install command) +「安装」(install) — install picks the package manager automatically (winget / built-in features on Windows, brew on macOS, apt/dnf/pacman on Linux, may need admin rights) and re-detects afterwards
|
|
29
|
+
|
|
30
|
+
### ② SSH Key & Connectivity
|
|
31
|
+
- **Platform selector**: GitHub (default) / Gitee — decides the SSH config target and connectivity test below
|
|
32
|
+
- **Auto-detect ed25519 key**: scans `~/.ssh/*.pub` for existing ed25519 public keys — prefers `id_ed25519`, otherwise the first found (any name, e.g. `github_ed25519`); falls back to `id_ed25519` when none exist. The status line shows the actual key filename, and the SSH config `IdentityFile` uses it too
|
|
33
|
+
- One-click **generate ed25519 key** (no passphrase; reuses an existing ed25519 key instead of duplicating)
|
|
34
|
+
- One-click **write SSH config** (GitHub: `github.com → ssh.github.com:443`; Gitee: `gitee.com` port 443) — see [Do I need the 443 config?](#do-i-need-the-443-config) below
|
|
35
|
+
- **Test connection** `ssh -T git@github.com` (GitHub) or `ssh -T git@gitee.com` (Gitee)
|
|
36
|
+
- Shows the public key content for easy copy-upload to the platform
|
|
37
|
+
- Detects whether `gh` is logged in and which account
|
|
38
|
+
|
|
39
|
+
### ③ Code Management
|
|
40
|
+
- **Follows ②'s platform**: all「detect / create / visibility」logic switches with the platform selector (GitHub via `gh` CLI, Gitee via Gitee OpenAPI)
|
|
41
|
+
- **Gitee token** (Gitee mode only): enter a Gitee personal access token (needs `projects` permission) → stored at `~/.dsh/storages/source-code-mgmt-gitee.json` on the machine (0600, **not inside the plugin dir**, never echoed to the browser/logs); one-click clear; invalid tokens are removed automatically
|
|
42
|
+
- **Workspace selector**: dropdown of DSH-registered workspace folders
|
|
43
|
+
- **Select folder →**: paste an absolute path or click「Browse…」for a native folder picker; confirmed folders are **persisted into a custom folder list** (`~/.dsh/storages/source-code-mgmt-dirs.json`, separate from the plugin dir — no personal paths leak), shown with a custom-folder badge and a ✕ to remove the entry (record only, never deletes the actual folder)
|
|
44
|
+
- Shows repo status: platform source, branch, remote, pending change count, ahead/behind remote, >100MB files
|
|
45
|
+
- **View details**: when there are changes, a「查看」(view) button opens a dialog listing changed/added/deleted/renamed **files or folders**. Every **text-previewable** file — including new/untracked files — can be expanded to show an inline **side-by-side diff** (old left / new right, deletions red, additions green); files with no old version (pure additions) show only the **「新版本」column**; **binary files show no「查看」button** (their content can't be previewed as text). When local and remote diverge, a view button on the sync row lists the concrete commits you're ahead/behind
|
|
46
|
+
- **Local Git workflow** (does not change the remote-sync logic):
|
|
47
|
+
- **Stage / unstage** per file in the changes dialog (distinguishing staged/unstaged by `git status` XY codes), with「已暂存 / 未暂存」markers
|
|
48
|
+
- **Commit message input + Commit button** above the repo name (git repos with changes only) — custom message, or auto-generated when left empty
|
|
49
|
+
- **Branch「Switch」** button — dialog listing branches, click to `checkout`
|
|
50
|
+
- **History button** — dialog listing commits (hash + subject + author + date), each with view (side-by-side diff), **revert**, **cherry-pick** (both with confirmation, as they rewrite history)
|
|
51
|
+
- **Remote matching by platform + current account**: ③'s「remote / sync」only counts remotes of the current platform **whose owner equals the currently logged-in account** (GitHub platform = `gh` account, Gitee platform = Gitee token account). So switching to Gitee never reads the GitHub origin; someone else's / another org's repos (e.g. `deepseek-ai/deepseek-harness`) are never treated as "your own remote", and ahead/behind is not computed for them. With no account-owned remote, the panel shows「(无)」and offers the same-name repo check + create-and-push flow
|
|
52
|
+
- **Init Git**: shown when the folder is **not** a git repo but a same-name repo already exists remotely — runs `git init` only (+ default identity), **no pull, no push**, you choose the next step
|
|
53
|
+
- **Create repo & push**: repo name defaults to the **folder name** (read-only), optional private/public; the button is disabled with a hint when a same-name repo already exists. For a brand-new directory with no commits, it auto-stages an initial commit before creating the repo to avoid "no commits found"
|
|
54
|
+
- GitHub: `gh repo create --private|--public --source=. --push`
|
|
55
|
+
- Gitee: Gitee OpenAPI `POST /user/repos` to create, then sets the SSH remote `git@gitee.com:<owner>/<name>.git` and `git push` (through ②'s SSH key)
|
|
56
|
+
|
|
57
|
+
### Data loading timing (fetch on open, "refreshing…" indicator)
|
|
58
|
+
DSH does **not** sync repos on startup — it only prefetches static env/SSH/workspace lists. Opening the plugin, switching workspace/folder, refreshing, and any push/pull/stage/commit operation fetch the latest repo status over the network and show a「⟳ 刷新中…」indicator — no stale data (like an old "no changes") when opening/reopening/switching.
|
|
59
|
+
|
|
60
|
+
## Do I need the 443 config?
|
|
61
|
+
|
|
62
|
+
Short answer for users outside mainland China / port-22-blocked networks: **usually no.**
|
|
63
|
+
|
|
64
|
+
- GitHub's standard SSH endpoint is `git@github.com` on **port 22**, and it works out of the box on almost every network outside mainland China.
|
|
65
|
+
- The plugin's「write SSH config」step writes GitHub's **officially supported** port-443 fallback (`Host github.com → HostName ssh.github.com, Port 443`). It exists for networks that block port 22 (common in mainland China, plus some corporate/school/campus or otherwise firewalled networks).
|
|
66
|
+
- The step is **fully optional and user-initiated** — you can simply skip it: generate the key → add the public key to GitHub → test connection → push. Everything runs over port 22.
|
|
67
|
+
- Writing it anyway is harmless (GitHub officially supports SSH over 443; the only edge case is networks that block 443 as well). Gitee is a China-hosted platform, so it's only relevant if you actually use Gitee.
|
|
68
|
+
|
|
69
|
+
## Installation
|
|
70
|
+
|
|
71
|
+
> This plugin ships as a **Profile Bundle**: its `package.json` declares `dsh.bundle` (carrying a `cordis.patch.yml` config layer), so `dsh plugin --profile web add` **installs and activates it in one step** — no manual config editing.
|
|
72
|
+
|
|
73
|
+
### Option 1: npm package (recommended)
|
|
74
|
+
|
|
75
|
+
Run this from **any directory** (the command locates/initializes the `web` profile itself):
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
dsh plugin --profile web add source-code-mgmt
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
> The command runs `pnpm add` in the web profile directory, then reconciles the plugin layer: because this package declares `dsh.bundle`, it is automatically appended to `dsh.profile.bundles` (see `~/.dsh/profiles/web/package.json`) and registered into the Cordis loader tree — **one command, done**.
|
|
82
|
+
|
|
83
|
+
After installing, **fully restart dsh web** (stop the old process — not a page refresh), then **F5** in the browser. The「Code Management」entry appears (sidebar Tab with dsh-better-sidebar, otherwise the header button + right panel).
|
|
84
|
+
|
|
85
|
+
### Option 2: local directory (development / testing)
|
|
86
|
+
|
|
87
|
+
**Windows (PowerShell):**
|
|
88
|
+
```powershell
|
|
89
|
+
# install local source (link: protocol — a symlink, source edits take effect immediately)
|
|
90
|
+
dsh plugin --profile web add link:C:/path/to/source-code-mgmt
|
|
91
|
+
dsh web
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
**Linux / macOS:**
|
|
95
|
+
```bash
|
|
96
|
+
dsh plugin --profile web add link:/home/yourname/path/to/source-code-mgmt
|
|
97
|
+
dsh web
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Option 3: from GitHub (distribution)
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
dsh plugin --profile web add git+https://github.com/Zhucy123/source-code-mgmt.git
|
|
104
|
+
dsh web
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
> A git install **copies** the source into node_modules — to pick up source changes, re-run `dsh plugin --profile web add ...` (unlike `link:`, which is a symlink).
|
|
108
|
+
|
|
109
|
+
### Verifying the install
|
|
110
|
+
|
|
111
|
+
After installing and restarting:
|
|
112
|
+
|
|
113
|
+
1. **Dependency written**: `source-code-mgmt` is in `dependencies` of `~/.dsh/profiles/web/package.json`.
|
|
114
|
+
2. **Added to the config layer**: `source-code-mgmt` is in the `dsh.profile.bundles` list of the same file (written automatically by `dsh plugin add` — no manual editing).
|
|
115
|
+
3. **Symlink created (`link:` only)**: `~/.dsh/profiles/web/node_modules/source-code-mgmt` points at your source dir (a Junction on Windows).
|
|
116
|
+
4. **Entry visible after restart**: sidebar「Code Management」Tab with dsh-better-sidebar, otherwise the header button beside "Session log" opening the right panel.
|
|
117
|
+
|
|
118
|
+
### Troubleshooting
|
|
119
|
+
|
|
120
|
+
| Symptom | Cause / fix |
|
|
121
|
+
|---------|-------------|
|
|
122
|
+
| Installed with `dsh plugin add` and restarted, but no button | Most common: the process was **refreshed, not fully restarted**. Stop the old `dsh web` process (it may still hold port 3080) and start it again. |
|
|
123
|
+
| Install prints「declares no dsh.bundle」 | The installed version lacks the bundle declaration (old version or a package missing `cordis.patch.yml`). Reinstall/update with version ≥ 1.9.0. |
|
|
124
|
+
|「Failed to load plugins」 | The host-side `lib/index.js` failed to boot (usually a dependency resolution issue). Check the startup log and confirm `node_modules` dependencies are complete. |
|
|
125
|
+
|
|
126
|
+
## Usage
|
|
127
|
+
|
|
128
|
+
1. Restart dsh web and refresh the browser.
|
|
129
|
+
2. Click the「**Code Management**」entry (sidebar Tab with dsh-better-sidebar, otherwise the header button beside "Session log").
|
|
130
|
+
3. ① Confirm Git / GitHub CLI are installed → ② generate a key and test connectivity → ③ pick a workspace, then push or create a new repo.
|
|
131
|
+
|
|
132
|
+
## Backend API routes
|
|
133
|
+
|
|
134
|
+
| Route | Method | Description |
|
|
135
|
+
|-------|--------|-------------|
|
|
136
|
+
| `/api/source-code-mgmt/env` | GET | Environment check (git/gh versions) |
|
|
137
|
+
| `/api/source-code-mgmt/install-tool` | POST | One-click install of a missing tool (body `tool`: `git`/`gh`/`ssh`, package manager picked per platform) |
|
|
138
|
+
| `/api/source-code-mgmt/ssh` | GET | SSH key / config / gh login status |
|
|
139
|
+
| `/api/source-code-mgmt/gen-key` | POST | Generate an ed25519 key |
|
|
140
|
+
| `/api/source-code-mgmt/write-config` | POST | Write SSH config (body `provider`: `github` default / `gitee`) |
|
|
141
|
+
| `/api/source-code-mgmt/ssh-test` | POST | Test SSH connectivity (body `provider`: `github` default / `gitee`) |
|
|
142
|
+
| `/api/source-code-mgmt/default-dir` | GET | Current workspace folder |
|
|
143
|
+
| `/api/source-code-mgmt/workspaces` | GET | List all workspace folders + custom folder set |
|
|
144
|
+
| `/api/source-code-mgmt/pick-dir` | POST | Native folder picker on the host; returns the chosen path |
|
|
145
|
+
| `/api/source-code-mgmt/add-workspace` | POST | Validate a folder and persist it to the custom list; returns the merged workspace list |
|
|
146
|
+
| `/api/source-code-mgmt/remove-workspace` | POST | Remove a custom-folder dropdown record only (never deletes the folder) |
|
|
147
|
+
| `/api/source-code-mgmt/align` | POST | Hard align: `git fetch` + `git reset --hard origin/<branch>` (full reset to remote) |
|
|
148
|
+
| `/api/source-code-mgmt/init-git` | POST | `git init` + default identity only — no pull/push |
|
|
149
|
+
| `/api/source-code-mgmt/repo-exists` | POST | Check whether a same-name repo exists (body `provider`: `github`/`gitee`) |
|
|
150
|
+
| `/api/source-code-mgmt/repo?dir=` | GET | Repo status (query `provider`: `github`/`gitee`) |
|
|
151
|
+
| `/api/source-code-mgmt/repo-diff?dir=&path=` | GET | Unified diff text for one changed file, on demand |
|
|
152
|
+
| `/api/source-code-mgmt/stage` | POST | Stage changes (body `dir`, `path`; empty `path` = all) |
|
|
153
|
+
| `/api/source-code-mgmt/unstage` | POST | Unstage changes (body `dir`, `path`; empty `path` = all) |
|
|
154
|
+
| `/api/source-code-mgmt/commit` | POST | Commit with a custom message (body `dir`, `message`, `paths?`) |
|
|
155
|
+
| `/api/source-code-mgmt/branches` | POST | List branches (current first) |
|
|
156
|
+
| `/api/source-code-mgmt/checkout` | POST | Switch branch (body `dir`, `branch`) |
|
|
157
|
+
| `/api/source-code-mgmt/log` | POST | Recent commit history (body `dir`, `count?`; returns hash/subject/author/date) |
|
|
158
|
+
| `/api/source-code-mgmt/revert` | POST | Revert a commit (body `dir`, `hash`) |
|
|
159
|
+
| `/api/source-code-mgmt/cherrypick` | POST | Cherry-pick a commit (body `dir`, `hash`) |
|
|
160
|
+
| `/api/source-code-mgmt/commit-diff` | POST | Full patch of a commit (body `dir`, `hash`) |
|
|
161
|
+
| `/api/source-code-mgmt/push` | POST | Commit and push (git operation, platform-agnostic) |
|
|
162
|
+
| `/api/source-code-mgmt/push-staged` | POST | Commit only staged content with your message, then push |
|
|
163
|
+
| `/api/source-code-mgmt/pull` | POST | `git pull --ff-only` (up-to-date / success / conflict feedback) |
|
|
164
|
+
| `/api/source-code-mgmt/merge-push` | POST | Pull and push (`git pull --rebase` + `git push`) |
|
|
165
|
+
| `/api/source-code-mgmt/force-push` | POST | Force push (`git push --force`) |
|
|
166
|
+
| `/api/source-code-mgmt/force-pull` | POST | Force pull (`git pull --force`) |
|
|
167
|
+
| `/api/source-code-mgmt/create` | POST | Create a repo and push (body `provider`; GitHub via `gh repo create`, Gitee via OpenAPI + SSH push) |
|
|
168
|
+
| `/api/source-code-mgmt/set-visibility` | POST | Change repo visibility (body `provider`; GitHub via `gh repo edit`, Gitee via `PATCH /repos/{owner}/{repo}`) |
|
|
169
|
+
| `/api/source-code-mgmt/gitee-token` | GET/POST | GET: token configured? + account; POST: save (`{token}`) or clear (`{clear:true}`) the Gitee token |
|
|
170
|
+
|
|
171
|
+
## Security
|
|
172
|
+
|
|
173
|
+
All routes are **loopback-only** (`sec-fetch-site` + Origin checks) — only the local machine's browser can call them; LAN/mobile sources get a 403, same policy as the control panel.
|
|
174
|
+
|
|
175
|
+
## Cross-platform
|
|
176
|
+
|
|
177
|
+
- **Windows / Linux / macOS**
|
|
178
|
+
- Platform detected via `process.platform`; `~/.ssh` resolved via `homedir()` (Windows: `C:\Users\<user>\.ssh`, Linux/macOS: `/home/<user>/.ssh` or `/Users/<user>/.ssh`)
|
|
179
|
+
- SSH config gets 0600 permissions on Linux/macOS
|
|
180
|
+
- **Automatic git / gh / ssh / ssh-keygen binary resolution** at startup: ① env overrides → ② PATH lookup (`.exe` added on Windows) → ③ (Windows only) Git's bundled dirs (`usr\bin` / `bin`), with the bare command name as a last resort. Git alone is enough even when `ssh` isn't on PATH — no per-machine config
|
|
181
|
+
- Optional explicit binary paths via env vars: `DSH_SCM_GIT` / `DSH_SCM_GH` / `DSH_SCM_SSH` / `DSH_SCM_SSH_KEYGEN`
|
|
182
|
+
- **SSH transport fix**: Git for Windows' bundled MSYS `ssh.exe` (`usr\bin\ssh.exe`) can fail with `couldn't create signal pipe, Win32 error 5` when spawned from a detached/agent process, breaking `git push`/`git pull`. The plugin injects `GIT_SSH` pointing at a working `ssh` (usually the system OpenSSH `C:\Windows\System32\OpenSSH\ssh.exe`) for git remote operations
|
|
183
|
+
- **Non-ASCII filename compatibility**: git's default `core.quotepath` prints paths with non-ASCII bytes as octal-escaped quoted strings (which display as garbled text). The plugin injects `-c core.quotepath=false` into every git invocation (raw UTF-8 output) and additionally unescapes any still-quoted paths via `parseGitPath()` — Chinese filenames display correctly in the changes list and in diff headers
|
|
184
|
+
- **One-click missing-tool install across platforms**: winget / built-in features (fallback choco/scoop) on Windows, brew on macOS, apt-get / dnf / pacman on Linux (auto `sudo -n`; skipped when already root). The native folder picker is Windows-only; on macOS/Linux paste the path into the input box instead
|
|
185
|
+
|
|
186
|
+
## Development
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
git clone https://github.com/Zhucy123/source-code-mgmt.git
|
|
190
|
+
cd source-code-mgmt
|
|
191
|
+
# test in your local DSH (installs to the web profile, auto-activated)
|
|
192
|
+
dsh plugin --profile web add link:$(pwd)
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- Changes to `lib/client.js` (browser side) → a page refresh is enough
|
|
196
|
+
- Changes to `lib/index.js` (host/Node side) → restart dsh web
|
|
197
|
+
|
|
198
|
+
## Version history
|
|
199
|
+
|
|
200
|
+
### v1.11.0 (current)
|
|
201
|
+
This release focuses on the changed-files preview experience and Chinese-filename compatibility:
|
|
202
|
+
|
|
203
|
+
- **New/untracked files are now viewable**: an untracked (新增) file row now shows「▸ 查看」and expands to display the file's full content (rendered as an "all added" diff; oversized files show the first 2000 lines with a truncation note, capped at 1 MB so huge files are never slurped into memory); an empty new file shows「(空文件)」.
|
|
204
|
+
- **No「旧版本」column for new files**: the side-by-side diff shows only the「新版本」column when the diff has no deletion rows (pure additions, e.g. new/untracked files); diffs with deletions keep the two-column comparison.
|
|
205
|
+
- **Binary files show no「查看」button**: each changed file is sniffed (NUL-byte heuristic, the same one git uses) to decide whether its content can be previewed as text — binary files (images, executables, …) get no「查看」button (tooltip: "Binary file — text preview unavailable"); files without a working-tree copy (deleted, or staged-then-removed) are judged via `git diff --numstat` (`-\t-` marks binary).
|
|
206
|
+
- **Chinese filename compatibility fix**: git's default `core.quotepath` prints Chinese paths as octal-escaped quoted strings (e.g. `"\346\270\270…md"`), which showed up garbled in the changes list and diff headers. Every git invocation now gets `-c core.quotepath=false` (raw UTF-8 paths), plus a defensive `parseGitPath()` unescaper applied to every path-parsing site — `status --porcelain`, `ls-files -z`, `diff --name-only`, and `git diff`/`git show` headers.
|
|
207
|
+
- New regression test `tools/verify-cn-paths.test.mjs` (Chinese path parsing, text/binary viewability, new-file diff generation, deleted-file detection).
|
|
208
|
+
|
|
209
|
+
### v1.10.1 (history)
|
|
210
|
+
- **Fix: the top-right「代码管理」button lingered even after dsh-better-sidebar was installed.** For the fallback entry (better-sidebar absent) the teardown handler was only wired for the ReactDOM fallback path — the slots path never stored it, so switching to the sidebar-Tab form left the header entry behind. The `slots.inject` disposer is now captured into `entryUnmount`, so switching to a Tab tears the header entry down correctly.
|
|
211
|
+
- **Fix: when better-sidebar is absent, the entry only showed inside a conversation and vanished in the new-conversation / no-conversation empty state.** The old entry lived in `conversation.session.header.utilities` (`scope: 'session'`), and the whole session header is hidden via `hideChrome` in the empty state. It is now **always present at the top-right**: with an active session it sits beside「Session log」(the right-aligned session-header utilities); in the new/no-conversation empty state it becomes a fixed top-right button registered in the always-mounted `shell.overlay` slot (shown only when the session is blank or absent, so it never duplicates the header button).
|
|
212
|
+
- **Fix: on refresh the「代码管理」button overlapped「Session log」.** `captureSessions()` takes ~1.2s to populate the session list, and before that the button wrongly believed there was no session and lit up early. The overlap button now renders nothing until the session list is captured.
|
|
213
|
+
- **Corrected visibility signal:** the fixed button now keys off whether the current session is blank (`sessions.list.getSnapshot().byId[current].blank`) — show only for blank/no-session, hide for an active (non-blank) session — replacing the inaccurate `current === undefined` check.
|
|
214
|
+
- **Robustness:** better-sidebar detection now uses a **bounded multi-tick retry** (fast start then slowing, ~44s, stopping on success and cleared on teardown) instead of a single 1.5s retry, covering slow client cold-start so the Tab-switch race no longer misses.
|
|
215
|
+
|
|
216
|
+
> All changes are client-side (`lib/client.js`); refresh the page to pick them up. Host `/api` routes and the push/ignore logic are untouched.
|
|
217
|
+
|
|
218
|
+
### v1.10.0 (history)
|
|
219
|
+
- **Bilingual UI, live (follows DSH's language setting)**: every piece of panel copy (steps ①②③, buttons, dialogs, status/result messages, confirm dialogs, the sidebar Tab title) plus host-side error/result messages is now driven by a bilingual dictionary. Language comes from DSH's Settings → General → Language and switches **instantly** — the panel re-renders via a `ctx.locale` subscription (the Tab title follows too), no refresh or restart. The host returns messages per-request based on `?lang=` (AsyncLocalStorage-scoped, so concurrent requests never cross languages). The Chinese UI is byte-identical to v1.9.0; English is a complete translation (including >100MB skip reasons, Gitee token hints, git command fallback messages). The `tools/` directory now holds the i18n extract/apply/test scripts for future maintenance.
|
|
220
|
+
|
|
221
|
+
### v1.9.0 (history)
|
|
222
|
+
- **Profile Bundle distribution — install = activate**: `dsh.bundle` changed from the bare string `"./lib/index.js"` to the object form `{ "patch": "./cordis.patch.yml" }`, with a new `cordis.patch.yml` (inserts the `source-code-mgmt` row). `dsh plugin --profile web add source-code-mgmt` now appends the package to `dsh.profile.bundles` and registers it into the Cordis loader tree automatically — **no manual `cordis.patch.yml` editing**. The「install ≠ activate」warning and the PowerShell activation script were removed from the README. Behavior is otherwise unchanged (same `lib/index.js` host half + `lib/client.js` browser half).
|
|
223
|
+
|
|
224
|
+
### v1.8.0 and earlier (history)
|
|
225
|
+
See the full Chinese changelog in [README.md](README.md#版本历史). Highlights of recent releases: push-staged button (v1.8.0), fetch-on-open with refreshing indicator (v1.7.0), header button + right panel when better-sidebar is absent (v1.6.0), one-click missing-tool install (v1.5.0), SSH key auto-detection (v1.4.0), local Git workflow — stage/unstage, custom commit message, branch switch, history with revert/cherry-pick, side-by-side diff (v1.3.0), adaptive entry + Gitee support (v1.1–1.2), first release (v1.0.0).
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
MIT
|
package/README.md
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
> [English](README.en.md) | 中文
|
|
4
4
|
|
|
5
|
-
> 版本:**v1.
|
|
5
|
+
> 版本:**v1.11.0** | 更新日志见文末「[版本历史](#版本历史)」
|
|
6
6
|
|
|
7
7
|
> **界面语言跟随 DSH 设置实时切换**:面板与 host 端消息自动使用 DSH 的语言(设置 → 通用 → 语言),中文 ↔ 英文即时生效,无需重启。
|
|
8
8
|
|
|
9
9
|
> DSH Web GUI 源代码管理插件:把「环境检查 → SSH 配置 → 代码上传推送」整合进「代码管理」面板,支持 GitHub / Gitee 双平台,一键管理代码仓库。
|
|
10
10
|
|
|
11
|
-
> 入口位置自适应:**已安装 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 时**,「代码管理」作为它侧边栏的一个新 Tab 页面出现(全新侧边栏 Tab
|
|
11
|
+
> 入口位置自适应:**已安装 [dsh-better-sidebar](https://github.com/omdsh-dev/DSH-better-sidebar) 时**,「代码管理」作为它侧边栏的一个新 Tab 页面出现(全新侧边栏 Tab);**未安装时**,「代码管理」按钮位于 DSH **右上角、常驻可见**——有会话时放在「Session 日志」旁边的右对齐列表里,无会话空态时改为一个固定在**右上角**的浮动按钮,点击后打开一个 **dsh-better-sidebar 外观的右侧集成面板**(推挤主内容区)。两种形态都复用同一套面板 UI。
|
|
12
12
|
|
|
13
13
|
## 功能
|
|
14
14
|
|
|
15
15
|
集成入口(二选一,自动检测,无需手动切换):
|
|
16
16
|
|
|
17
17
|
- **已安装 dsh-better-sidebar**:「代码管理」注册为它侧边栏的一个**新 Tab 页面**,点击侧边栏 Tab 直接打开面板;
|
|
18
|
-
- **未安装 dsh-better-sidebar
|
|
18
|
+
- **未安装 dsh-better-sidebar**:「代码管理」按钮常驻在 DSH **右上角**——有活跃(非空白)会话时通过 `conversation.session.header.utilities` 槽位放在「Session 日志」旁(同款胶囊、间距一致);空白(新对话)/无会话空态时通过常驻的 `shell.overlay` 槽注册一个固定在右上角的浮动按钮(仅空白或没有会话时显示,活跃会话时交给 header 内的按钮,避免重复)。点击都打开一个 **dsh-better-sidebar 外观的右侧集成面板**(内容放同一面板),并把主内容区往左推挤。
|
|
19
19
|
|
|
20
|
-
> 检测只是激活时一次内存读取(`ctx.get('betterSidebar')`),零 I/O、零网络,不影响 DSH
|
|
20
|
+
> 检测只是激活时一次内存读取(`ctx.get('betterSidebar')`),零 I/O、零网络,不影响 DSH 启动速度;两种形态间自动切换。未安装 better-sidebar 时入口常驻右上角(有会话=Session 日志旁,空态=固定右上角浮动按钮),空态/新对话也可见。
|
|
21
21
|
|
|
22
22
|
面板分三步:
|
|
23
23
|
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
- **选择工作区**:下拉选择 DSH 已登记的工作区文件夹,选中即加载
|
|
45
45
|
- **选择目录 →**:下拉右侧按钮,可手动输入/粘贴目录绝对路径或点击「浏览…」弹出原生文件夹选择器;确认后**持久化加入自定义目录列表**(插件独立存储于 `~/.dsh/storages/source-code-mgmt-dirs.json`,**不写入插件目录**,开源不泄漏个人路径),下次打开无需重新选择;手动添加的目录会以**自定义目录徽标**显示,末端带 **✕** 可一键删除该下拉记录(只删记录,不删实际文件夹)
|
|
46
46
|
- 显示仓库状态:平台来源、分支、远程地址、待提交改动数、领先/落后远程、>100MB 文件
|
|
47
|
-
- **查看详情**:有改动时「改动」行旁出现「查看」按钮 →
|
|
47
|
+
- **查看详情**:有改动时「改动」行旁出现「查看」按钮 → 点击弹出窗口列出改动/新增/删除/重命名的**文件或文件夹名称**;所有**可文本预览**的文件(含新增/untracked 新文件)都可**点击文件行展开查看内容**——**并排视图**(左旧右新,删除行红底、新增行绿底),**纯新增文件没有旧版本,只显示「新版本」一列**;**二进制文件不显示「查看」**(内容无法按文本预览);本地与远程存在差异时「同步」行旁出现「查看」按钮 → 点击弹出窗口显示**本地领先/落后的具体提交列表**;无改动或已一致时不显示按钮
|
|
48
48
|
- **本地 Git 工作流**(不改动远程同步逻辑):
|
|
49
49
|
- 「改动」弹窗里每个文件行有**暂存 / 取消暂存**按钮(按 `git status` 的 XY 状态区分 staged/unstaged),并显示「已暂存 / 未暂存」标记
|
|
50
50
|
- ③ 面板仓库名上方有**提交信息输入框 + 「提交」按钮**(仅 git 仓库且有改动时显示)——可写自定义提交信息,**不再用固定 message**;留空则自动生成
|
|
@@ -181,6 +181,7 @@ dsh web
|
|
|
181
181
|
- **git / gh / ssh / ssh-keygen 二进制自动探测**:启动时依次按 ①环境变量覆盖 → ②PATH 查找(Windows 加 `.exe`)→(仅 Windows)③Git 自带目录(`usr\bin` / `bin`)回退,最后兜底用裸命令名。因此只要装了 Git,即使 `ssh` 不在 PATH 里也能正常工作,换设备无需额外配置。
|
|
182
182
|
- 如需手动指定二进制路径,可用环境变量覆盖:`DSH_SCM_GIT` / `DSH_SCM_GH` / `DSH_SCM_SSH` / `DSH_SCM_SSH_KEYGEN`
|
|
183
183
|
- **SSH 传输修复**:Git for Windows 自带的 MSYS `ssh.exe`(`usr\bin\ssh.exe`)在被 detached/agent 进程调用时可能报 `couldn't create signal pipe, Win32 error 5`,导致 `git push`/`git pull` 失败。插件执行 git 远程命令时会自动注入 `GIT_SSH` 指向解析到的可用 `ssh`(通常为系统 OpenSSH `C:\Windows\System32\OpenSSH\ssh.exe`),避免该问题。
|
|
184
|
+
- **中文文件名兼容**:git 默认 `core.quotepath` 会把含非 ASCII 字节的路径输出成八进制转义的引号串(显示为乱码)。插件对所有 git 调用统一注入 `-c core.quotepath=false`(直接输出原始 UTF-8 路径),并对仍带引号转义的路径做 `parseGitPath()` 反转义兜底——改动列表 / diff 里的中文文件名显示正常
|
|
184
185
|
- **缺工具一键安装跨平台**:Windows 用 winget / 内置功能(回退 choco/scoop),macOS 用 brew,Linux 用 apt-get / dnf / pacman(自动带 `sudo -n`;已是 root 则省略)。SSH 密钥探测、本地 Git 工作流、并排 diff 等在三个平台行为一致;「浏览目录」的原生选择器仅 Windows 可用,macOS/Linux 上请在输入框直接填路径(可手动输入粘贴)。
|
|
185
186
|
|
|
186
187
|
## 开发
|
|
@@ -197,7 +198,27 @@ dsh plugin --profile web add link:$(pwd)
|
|
|
197
198
|
|
|
198
199
|
## 版本历史
|
|
199
200
|
|
|
200
|
-
### v1.
|
|
201
|
+
### v1.11.0(当前)
|
|
202
|
+
本次更新(改动文件查看体验 + 中文文件名兼容性):
|
|
203
|
+
|
|
204
|
+
- **新增/untracked 文件现在可以查看内容**:改动列表里「新增」文件行也显示「▸ 查看」,点击展开显示完整文件内容(按「全部新增」的 diff 渲染,超大文件只显示前 2000 行并附截断提示,上限 1MB 不会整读进内存);空的新文件显示「(空文件)」。
|
|
205
|
+
- **新增文件不再显示「旧版本」列**:并排 diff 在没有任何删除行(纯新增,如新增/untracked 文件)时只显示「新版本」一列,不再出现空白的「旧版本」表头与左列;有增有删的修改仍保留双列对比。
|
|
206
|
+
- **二进制文件不显示「查看」**:每个改动文件按内容嗅探(NUL 字节启发式,与 git 一致)判断是否可文本预览——二进制文件(图片、exe 等)不显示「查看」按钮(悬停提示「二进制文件,无法查看文本内容」);已删除/暂存后无副本的文件用 `git diff --numstat`(二进制条目 `-\t-`)判断。
|
|
207
|
+
- **中文文件名兼容性修复**:git 默认 `core.quotepath` 会把中文路径输出成八进制转义的引号串(如 `"\346\270\270…md"`),导致改动列表与 diff 头部的中文文件名显示成乱码。现在所有 git 调用统一注入 `-c core.quotepath=false`(直接输出 UTF-8 路径),并新增 `parseGitPath()` 反转义兜底——改动列表、`ls-files -z`、`diff --name-only`、`git diff`/`git show` 头部路径等解析点全覆盖。
|
|
208
|
+
- 新增 `tools/verify-cn-paths.test.mjs` 回归测试(中文路径解析、文本/二进制可查看性、新增文件 diff 生成、已删除文件判断)。
|
|
209
|
+
|
|
210
|
+
### v1.10.1(历史)
|
|
211
|
+
本次更新(修复 better-sidebar 集成与「未安装 better-sidebar」时入口的若干问题):
|
|
212
|
+
|
|
213
|
+
- **修复:装了 dsh-better-sidebar 后,右上角「代码管理」按钮仍残留**。根因:未装 better-sidebar 时的降级入口此前只给 ReactDOM 降级路径赋了 `entryUnmount`,slots 路径漏了——导致切到「侧边栏 Tab」形态时 header 入口没被拆掉。现已把 `slots.inject` 返回的 disposer 记进 `entryUnmount`,切到 Tab 时正确拆除(顺带修正同类清理逻辑)。
|
|
214
|
+
- **修复:未安装 better-sidebar 时,入口只在对话内显示,新对话/无会话空态不显示**。根因:旧入口挂 `conversation.session.header.utilities`(`scope: 'session'`),而空态时整个会话 header 被 `hideChrome` 隐藏,按钮出不来。现在改为**右上角常驻**——有活跃会话时放在「Session 日志」旁(会话 header 的右对齐列表),新对话/无会话空态时改为 `shell.overlay` 常驻槽里 `position: fixed` 钉在右上角的按钮(仅空白/无会话时显示,避免与 header 按钮重复)。
|
|
215
|
+
- **修复:刷新瞬间「代码管理」按钮与「Session 日志」重叠**。根因:`captureSessions()` 延迟 1.2s 才捕获到 session 列表,捕获前误判为「无会话」而提前点亮常驻按钮。现在未捕获到 session 列表前不渲染常驻按钮,杜绝刷新闪叠。
|
|
216
|
+
- **判定信号修正**:常驻按钮的显隐改用「当前会话是否空白」判断(`sessions.list.getSnapshot().byId[current].blank`)——活跃(非空白)会话才隐藏常驻按钮(交给 header 内的按钮),空白/无会话则显示,替代原来不准确的「`current === undefined`」判断。
|
|
217
|
+
- **健壮性**:better-sidebar 服务的探测由「一次性 1.5s 重试」改为**有界多档重试**(快节奏起步再放慢,覆盖约 44s,拿到即停、卸载即清),兜底 better-sidebar 客户端冷启动较慢导致漏切 Tab 的竞态。
|
|
218
|
+
|
|
219
|
+
> 以上均为客户端(`lib/client.js`)改动,刷新页面即生效;host 端 `/api` 路由与推送/忽略逻辑未改动。
|
|
220
|
+
|
|
221
|
+
### v1.10.0(历史)
|
|
201
222
|
本次更新:
|
|
202
223
|
|
|
203
224
|
- **界面中英文实时切换(跟随 DSH 语言设置)**:面板全部文案(①②③ 三步、按钮、弹窗、状态/结果消息、确认框、Tab 标题)与 host 端错误/结果消息改为双语词典驱动——语言 = DSH 设置 → 通用 → 语言,切换**即时生效**(面板经 `ctx.locale` 订阅实时重渲染,Tab 标题随动),无需刷新/重启;host 端按请求的 `?lang=` 经 AsyncLocalStorage 按请求返回对应语言,并发不串扰。中文界面与 v1.9.0 完全一致,英文界面为完整翻译(含 >100MB 忽略原因、Gitee 令牌提示、git 命令失败回退等全部消息)。新增 `tools/` 下的 i18n 提取/应用/测试脚本便于后续维护
|
package/cordis.patch.yml
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
# Profile bundle: mount the「代码管理」plugin when a profile lists this bundle.
|
|
2
|
-
#
|
|
3
|
-
# One row covers both halves:
|
|
4
|
-
# - host half: `lib/index.js` (package main) injects webServer and registers
|
|
5
|
-
# the /api/source-code-mgmt/* routes;
|
|
6
|
-
# - browser half: the modules node half scans enabled Loader entries whose
|
|
7
|
-
# manifest declares `dsh.client` and serves `lib/client.js` under /plugins,
|
|
8
|
-
# so the same row enables the「代码管理」entry in the web UI.
|
|
9
|
-
#
|
|
10
|
-
# `dsh plugin --profile <name> add source-code-mgmt` appends this bundle to the
|
|
11
|
-
# profile's `dsh.profile.bundles` automatically; no manual cordis.patch.yml
|
|
12
|
-
# editing is needed.
|
|
13
|
-
- insert:
|
|
14
|
-
- id: source-code-mgmt
|
|
15
|
-
name: 'source-code-mgmt'
|
|
1
|
+
# Profile bundle: mount the「代码管理」plugin when a profile lists this bundle.
|
|
2
|
+
#
|
|
3
|
+
# One row covers both halves:
|
|
4
|
+
# - host half: `lib/index.js` (package main) injects webServer and registers
|
|
5
|
+
# the /api/source-code-mgmt/* routes;
|
|
6
|
+
# - browser half: the modules node half scans enabled Loader entries whose
|
|
7
|
+
# manifest declares `dsh.client` and serves `lib/client.js` under /plugins,
|
|
8
|
+
# so the same row enables the「代码管理」entry in the web UI.
|
|
9
|
+
#
|
|
10
|
+
# `dsh plugin --profile <name> add source-code-mgmt` appends this bundle to the
|
|
11
|
+
# profile's `dsh.profile.bundles` automatically; no manual cordis.patch.yml
|
|
12
|
+
# editing is needed.
|
|
13
|
+
- insert:
|
|
14
|
+
- id: source-code-mgmt
|
|
15
|
+
name: 'source-code-mgmt'
|
package/lib/client.js
CHANGED
|
@@ -114,9 +114,9 @@ window.__ModuleLoader__.load({
|
|
|
114
114
|
if (!f || typeof f !== "object") continue
|
|
115
115
|
const key = dir + "\u0000" + f.path
|
|
116
116
|
if (cache.diffs[key] !== undefined) continue
|
|
117
|
+
// 二进制等无法按文本预览的文件(列表里不显示「查看」),跳过预取。
|
|
118
|
+
if (f.viewable === false) { cache.diffs[key] = null; continue }
|
|
117
119
|
try {
|
|
118
|
-
// untracked 新文件 git 不跟踪,无 diff,直接标记为空。
|
|
119
|
-
if (f.type === "untracked") { cache.diffs[key] = ""; continue }
|
|
120
120
|
const r = await jget("/repo-diff?dir=" + encodeURIComponent(dir) + "&path=" + encodeURIComponent(f.path)).catch(() => null)
|
|
121
121
|
cache.diffs[key] = (r && typeof r.diff === "string") ? r.diff : null
|
|
122
122
|
} catch { cache.diffs[key] = null }
|
|
@@ -179,9 +179,10 @@ window.__ModuleLoader__.load({
|
|
|
179
179
|
const h = React.createElement;
|
|
180
180
|
|
|
181
181
|
// ---------- i18n: follows DSH’s language setting (Settings → General → Language), live ----------
|
|
182
|
-
const EN_DICT = {"展开":"Expand","折叠":"Collapse","展开该部分":"Expand this section","折叠该部分":"Collapse this section","已安装":"Installed","❌ 未安装":"❌ Not installed","复制安装命令":"Copy install command","一键安装(会自动选包管理器)":"Install automatically (picks a package manager)","安装中…":"Installing…","安装":"Install"," 执行成功":" executed successfully","(如未生效需以管理员身份重试)":" (retry as administrator if it did not take effect)","安装失败":"Install failed","① 环境检查":"① Environment","✅ 均存在":"✅ All present","操作系统":"OS","已找到":"Found","检测中":"Checking","失败: ":"Failed: ","未找到的工具可用上方「安装」按钮一键安装(":"Missing tools can be installed with the \"Install\" button above (","Windows 用 winget / 内置功能":"winget / built-in features on Windows","用系统包管理器":"your system package manager",",可能需管理员权限);也可手动复制安装命令执行,安装后点「重新检查」。":"; admin rights may be required). You can also copy the install command and run it manually, then click \"Re-check\".","重新检查":"Re-check",":已存在,无需重复操作":": already exists, no need to repeat","成功":" succeeded","失败":" failed","失败:":"Failed: ","平台":"Platform","选择代码托管平台:GitHub 或 Gitee(默认 GitHub),③代码管理会跟随切换":"Choose the code hosting platform: GitHub or Gitee (GitHub default); step ③ follows this switch","GitHub(默认)":"GitHub (default)","② SSH 密钥与连接":"② SSH Key & Connection","密钥":"Key"," 已生成":" generated","⚠️ 未生成":"⚠️ Not generated","GH 登录":"GH login","已登录":"Logged in","⚠️ 未登录":"⚠️ Not logged in","SSH 配置":"SSH config"," 已配置(443)":" configured (443)","⚠️ 未配置/限制 22 端口需配置":"⚠️ Not configured / port 22 blocked — use 443","生成密钥":"Generate key","配置 SSH(config)":"Configure SSH config","配置 SSH":"Configure SSH","测试连接":"Test connection","SSH 连接成功(":"SSH connection succeeded (","已认证":"authenticated","连接失败,请确认密钥已上传到 ":"Connection failed. Make sure the key is uploaded to "," 或已登录 gh":" or that gh is logged in","测试失败:":"Test failed: ","公钥(复制上传到 ":"Public key (copy and upload to ","Gitee → 设置 → SSH 公钥":"Gitee → Settings → SSH keys",",或运行 gh auth login 自动上传):":", or run gh auth login to upload it automatically):","该目录不是 git 仓库":"This folder is not a git repository","读取令牌状态失败:":"Failed to read token status: ","已保存 Gitee 令牌(账号:":"Gitee token saved (account: ","保存令牌失败":"Failed to save token","保存令牌失败:":"Failed to save token: ","已清除 Gitee 令牌":"Gitee token cleared","清除失败":"Clear failed","清除令牌失败:":"Failed to clear token: ","已推送":"Pushed","推送失败":"Push failed","推送失败:":"Push failed: ","已拉取远程更新(本地已对齐到远程最新状态)":"Pulled remote updates (local is now aligned with the latest remote state)","拉取失败":"Pull failed","拉取失败:":"Pull failed: ","已拉取远程更新并推送更改":"Pulled remote updates and pushed changes","合并推送失败":"Merge-push failed","合并推送失败:":"Merge-push failed: ","强制推送会用本地版本覆盖远程仓库,远程上非本地的更改将被丢弃。确定继续?":"Force-push overwrites the remote repo with your local version; remote-only changes will be lost. Continue?","已强制推送,远程已更新为本地状态":"Force-pushed; the remote is now your local state","强制推送失败":"Force-push failed","强制推送失败:":"Force-push failed: ","强制拉取会把远程更新并入本地。如果本地有不想保留的内容,将按冲突处理或遗失。确定继续?":"Force-pull merges remote updates into local; anything you don't want kept may conflict or be lost. Continue?","已强制拉取远程更新":"Force-pulled remote updates","强制拉取失败":"Force-pull failed","强制拉取失败:":"Force-pull failed: ","请先加载一个有效的文件夹":"Load a valid folder first","确定把仓库 ":"Change repo "," 改为「":" visibility to「","公开":"Public","私有":"Private","」?修改可见性可能影响 Star、关注者等。":"」? Changing visibility may affect stars, watchers, etc.","已把仓库设置为「":"Repo set to「","修改可见性失败":"Failed to change visibility","修改可见性失败:":"Failed to change visibility: ","仓库已创建并推送:":"Repo created and pushed: ","创建失败":"Create failed","创建失败:":"Create failed: ","未选择目录":"No folder selected","选择失败:":"Selection failed: ","请输入或选择目录路径":"Enter or pick a folder path","添加目录失败":"Failed to add folder","添加目录失败:":"Failed to add folder: ","删除失败":"Delete failed","删除失败:":"Delete failed: ","强制对齐会用远程分支覆盖本地(丢弃本地未推送的更改与提交),确定继续?":"Force-align overwrites local with the remote branch (drops unpushed local changes and commits). Continue?","已强制对齐到远程分支":"Force-aligned to the remote branch","强制对齐失败":"Force-align failed","强制对齐失败:":"Force-align failed: ","已是 git 仓库":"Already a git repository","已创建 git 仓库,请自行拉取或推送":"Git repository created — pull or push as you wish","创建 git 失败":"Failed to init git","创建 git 失败:":"Failed to init git: ","暂存失败":"Stage failed","暂存失败:":"Stage failed: ","取消暂存失败":"Unstage failed","取消暂存失败:":"Unstage failed: ","已提交 ":"Committed "," 个文件":" file(s)","提交失败":"Commit failed","提交失败:":"Commit failed: ","已提交「":"Committed「","(自动生成)":" (auto-generated)","」并推送":"」 and pushed","已推送本地已有的提交":"Pushed existing local commits","读取分支失败":"Failed to load branches","读取分支失败:":"Failed to load branches: ","切换到分支「":"Switch to branch「","」?(若当前有未提交改动,git 会拒绝切换)":"」? (git will refuse if you have uncommitted changes)","已切换到分支 ":"Switched to branch ","切换分支失败":"Failed to switch branch","切换分支失败:":"Failed to switch branch: ","读取历史失败":"Failed to load history","读取历史失败:":"Failed to load history: ","revert 提交「":"Revert commit「","」——会生成一个反向提交,期间可能产生冲突,确定继续?":"」 — this creates a reverse commit and may cause conflicts. Continue?","已 revert 提交 ":"Reverted commit ","revert 失败":"Revert failed","revert 失败:":"Revert failed: ","cherry-pick 提交「":"Cherry-pick commit「","」到当前分支——期间可能产生冲突,确定继续?":"」 onto the current branch — this may cause conflicts. Continue?","已 cherry-pick 提交 ":"Cherry-picked commit ","cherry-pick 失败":"Cherry-pick failed","cherry-pick 失败:":"Cherry-pick failed: ","读取失败":"Failed to load","已复制":"Copied","短哈希":"short hash","完整哈希":"full hash","提交信息":"commit message","复制失败:请手动复制":"Copy failed — copy it manually","③ 代码管理":"③ Code Management","Gitee 私人令牌(OpenAPI,需 projects 权限)":"Gitee personal token (OpenAPI, requires projects permission)","✅ 已配置":"✅ Configured","(账号 ":" (account ","清除令牌":"Clear token","粘贴 Gitee 私人令牌(https://gitee.com/personal_access_tokens)":"Paste the Gitee personal token (https://gitee.com/personal_access_tokens)","保存令牌":"Save token","令牌只保存在本机 ~/.dsh/storages(0600),不会写进插件目录;需勾选个人令牌的 projects 权限。公钥需已上传到 Gitee(② 里检查)。":"The token is stored only on this machine at ~/.dsh/storages (0600), never in the plugin directory; the personal token must have projects permission. Your public key must be uploaded to Gitee (checked in ②).","选择工作区":"Workspace","选择 DSH 已登记的工作区文件夹":"Choose a workspace folder registered in DSH","(暂无可选工作区)":"(no workspaces available)","— 请选择 —":"— Select —","选择目录":"Choose folder","自定义目录:":"Custom folder: ","删除该目录记录":"Remove this folder entry","删除该目录记录(不删除实际文件夹)":"Remove this folder entry (does not delete the actual folder)","目录":"Folder","分支":"Branch","(无)":"(none)","切换分支":"Switch branch","切换":"Switch","查看提交历史":"View commit history","历史":"History","远程":"Remote","改动":"Changes","无":"None","查看改动的文件列表":"View the list of changed files","查看":"View","同步":"Sync","本地领先 ":"Ahead by "," 提交":" commit(s)","、落后 ":", behind by ","落后 ":"behind by ","与远程一致":"Up to date with remote","查看本地与远程的提交差异":"View commit differences between local and remote","⚠️ 以下 ":"⚠️ The following "," 项 >100MB(超过 GitHub 限制,推送时将自动忽略不上传):":" item(s) >100MB (over GitHub's limit — auto-ignored on push):","/(整个文件夹)":"/ (entire folder)","创建 Git":"Init Git","仅初始化 git 仓库,不拉取不推送,由你决定下一步":"Initializes a git repo only — no pull/push; you decide the next step","本地有改动且远程有更新,可直接用「强制对齐」将本地重置为远程状态":"You have local changes AND remote updates — use \"Force align\" to reset local to the remote state","强制对齐":"Force align","本地完全重置为远程分支(丢弃本地差异),解决“文件相同仍显示同步差异”的情况":"Fully resets local to the remote branch (drops local differences); fixes \"files identical but sync still shows a difference\"","推送更改":"Push changes","拉取更新":"Pull updates","✓ 已是最新":"✓ Up to date","⟳ 正在刷新状态,联网同步 GitHub/Gitee 最新数据…":"⟳ Refreshing status — syncing the latest GitHub/Gitee data…","⟳ 切换平台,正在重新检测「":"⟳ Switching platform, re-checking「","」仓库状态…":"」 repo status…","推送中…":"Pushing…","推送暂存":"Push staged","把已暂存的内容用填写的(或自动生成的)信息提交后推送到远程;不会自动暂存其他未暂存的改动":"Commits only what is staged (with your message, or auto-generated) and pushes it; does not auto-stage other changes","刷新中…":"Refreshing…","刷新状态":"Refresh status","提交信息(留空则用默认)":"Commit message (default if empty)","填写提交信息后点「提交」;留空则自动生成(chore: update <文件夹名>)":"Type a message then click \"Commit\"; leave empty to auto-generate (chore: update <folder>)","提交":"Commit","仓库名称自动取文件夹名(不可修改)":"Repo name is taken from the folder name (read-only)","(未加载文件夹)":"(no folder loaded)","仓库可见性:私有 / 公开":"Repo visibility: private / public","(修改当前仓库的可见性)":" (changes the current repo's visibility)","新建仓库并推送":"Create repo & push","修改仓库状态":"Change repo status","✓ 仓库已是「":"✓ Repo is already「","」状态,如需修改请调整左侧可见性选择。":"」 — to change it, adjust the visibility dropdown.","将把仓库从「":"Will change the repo from「","」改为「":"」 to「","」,点击「修改仓库状态」执行。":"」 — click \"Change repo status\" to apply.","⚠️ 同名仓库已经创建(无法读取当前可见性,可能未":"⚠️ A repo with this name already exists (couldn't read its visibility — maybe not ","配置 Gitee 令牌":"Gitee token configured","登录 gh":"logged into gh","✓ 同名仓库 ":"✓ No repo named "," 不存在,可在 ":" exists — you can create it on ","「新建仓库并推送」创建(可选私有/公开)。":" with \"Create repo & push\" (private or public).","已忽略未上传(":"Skipped uploads ("," 项 >100MB):":" item(s) >100MB):","已提交":"Committed"," 并推送":" and pushed","(推送省略)":" (push skipped)","查看详情":"View details","改动文件":"Changed files","提交历史":"Commit history","与远程同步差异":"Sync differences vs remote","关闭":"Close","已安装 dsh-better-sidebar,此处只列改动文件列表;具体改动内容请到 dsh-better-sidebar 的「源代码管理 / Git 面板」查看。":"dsh-better-sidebar is installed — this lists changed files only; see the actual changes in its \"Source Control / Git\" panel.","点击收起":"Click to collapse","点击查看改动内容":"Click to view the diff","新文件(untracked)暂无内容 diff":"New (untracked) file — no diff yet","已暂存":"Staged","未暂存":"Unstaged","取消暂存":"Unstage","暂存":"Stage","加载中…":"Loading…","▾ 收起":"▾ Collapse","▸ 查看":"▸ View","正在加载改动内容…":"Loading changes…","(无内容差异)":"(no diff)","当前没有改动。":"No changes.","正在读取分支…":"Loading branches…","(无分支)":"(no branches)","(当前)":"(current)","正在读取历史…":"Loading history…","(暂无提交)":"(no commits)","更多操作(右键也可打开)":"More actions (right-click also works)","查看提交差异":"View commit diff","复制短哈希":"Copy short hash","复制完整哈希":"Copy full hash","复制提交信息":"Copy commit message","还原此提交":"Revert this commit","拾取此提交":"Cherry-pick this commit","正在加载提交 diff…":"Loading commit diff…","本地落后 ":"Behind by "," 个提交(远程有而本地没有):":" commit(s) (on remote, not local):"," 个提交(本地有而远程没有):":" commit(s) (on local, not remote):","✓ 已与远程同步,无差异。":"✓ In sync with remote — no differences.","选择代码目录":"Choose a code folder","可手动输入/粘贴目录绝对路径,或点击「浏览…」弹出本地文件夹选择器。确认后将添加到下方下拉并记住,下次打开无需重新选择。":"Paste an absolute folder path, or click \"Browse…\" for the native picker. Confirmed folders are added to the dropdown and remembered.","C:\\Users\\你的用户名\\项目目录":"C:\\Users\\your-name\\project","浏览…":"Browse…","取消":"Cancel","确定":"OK","新增":"Added","删除":"Deleted","重命名":"Renamed","修改":"Modified","旧版本":"Old","新版本":"New","源代码管理":"Source Control","按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理。推送会自动忽略 >100MB 的文件(":"Work through in order: ① Environment → ② SSH Key & Connection → ③ Code Management. Files over 100MB are auto-ignored on push ("," 限制)并说明原因。":" limit) with the reason shown.","代码管理":"Code Management","拖动调整面板宽度":"Drag to resize the panel"};
|
|
182
|
+
const EN_DICT = {"展开":"Expand","折叠":"Collapse","展开该部分":"Expand this section","折叠该部分":"Collapse this section","已安装":"Installed","❌ 未安装":"❌ Not installed","复制安装命令":"Copy install command","一键安装(会自动选包管理器)":"Install automatically (picks a package manager)","安装中…":"Installing…","安装":"Install"," 执行成功":" executed successfully","(如未生效需以管理员身份重试)":" (retry as administrator if it did not take effect)","安装失败":"Install failed","① 环境检查":"① Environment","✅ 均存在":"✅ All present","操作系统":"OS","已找到":"Found","检测中":"Checking","失败: ":"Failed: ","未找到的工具可用上方「安装」按钮一键安装(":"Missing tools can be installed with the \"Install\" button above (","Windows 用 winget / 内置功能":"winget / built-in features on Windows","用系统包管理器":"your system package manager",",可能需管理员权限);也可手动复制安装命令执行,安装后点「重新检查」。":"; admin rights may be required). You can also copy the install command and run it manually, then click \"Re-check\".","重新检查":"Re-check",":已存在,无需重复操作":": already exists, no need to repeat","成功":" succeeded","失败":" failed","失败:":"Failed: ","平台":"Platform","选择代码托管平台:GitHub 或 Gitee(默认 GitHub),③代码管理会跟随切换":"Choose the code hosting platform: GitHub or Gitee (GitHub default); step ③ follows this switch","GitHub(默认)":"GitHub (default)","② SSH 密钥与连接":"② SSH Key & Connection","密钥":"Key"," 已生成":" generated","⚠️ 未生成":"⚠️ Not generated","GH 登录":"GH login","已登录":"Logged in","⚠️ 未登录":"⚠️ Not logged in","SSH 配置":"SSH config"," 已配置(443)":" configured (443)","⚠️ 未配置/限制 22 端口需配置":"⚠️ Not configured / port 22 blocked — use 443","生成密钥":"Generate key","配置 SSH(config)":"Configure SSH config","配置 SSH":"Configure SSH","测试连接":"Test connection","SSH 连接成功(":"SSH connection succeeded (","已认证":"authenticated","连接失败,请确认密钥已上传到 ":"Connection failed. Make sure the key is uploaded to "," 或已登录 gh":" or that gh is logged in","测试失败:":"Test failed: ","公钥(复制上传到 ":"Public key (copy and upload to ","Gitee → 设置 → SSH 公钥":"Gitee → Settings → SSH keys",",或运行 gh auth login 自动上传):":", or run gh auth login to upload it automatically):","该目录不是 git 仓库":"This folder is not a git repository","读取令牌状态失败:":"Failed to read token status: ","已保存 Gitee 令牌(账号:":"Gitee token saved (account: ","保存令牌失败":"Failed to save token","保存令牌失败:":"Failed to save token: ","已清除 Gitee 令牌":"Gitee token cleared","清除失败":"Clear failed","清除令牌失败:":"Failed to clear token: ","已推送":"Pushed","推送失败":"Push failed","推送失败:":"Push failed: ","已拉取远程更新(本地已对齐到远程最新状态)":"Pulled remote updates (local is now aligned with the latest remote state)","拉取失败":"Pull failed","拉取失败:":"Pull failed: ","已拉取远程更新并推送更改":"Pulled remote updates and pushed changes","合并推送失败":"Merge-push failed","合并推送失败:":"Merge-push failed: ","强制推送会用本地版本覆盖远程仓库,远程上非本地的更改将被丢弃。确定继续?":"Force-push overwrites the remote repo with your local version; remote-only changes will be lost. Continue?","已强制推送,远程已更新为本地状态":"Force-pushed; the remote is now your local state","强制推送失败":"Force-push failed","强制推送失败:":"Force-push failed: ","强制拉取会把远程更新并入本地。如果本地有不想保留的内容,将按冲突处理或遗失。确定继续?":"Force-pull merges remote updates into local; anything you don't want kept may conflict or be lost. Continue?","已强制拉取远程更新":"Force-pulled remote updates","强制拉取失败":"Force-pull failed","强制拉取失败:":"Force-pull failed: ","请先加载一个有效的文件夹":"Load a valid folder first","确定把仓库 ":"Change repo "," 改为「":" visibility to「","公开":"Public","私有":"Private","」?修改可见性可能影响 Star、关注者等。":"」? Changing visibility may affect stars, watchers, etc.","已把仓库设置为「":"Repo set to「","修改可见性失败":"Failed to change visibility","修改可见性失败:":"Failed to change visibility: ","仓库已创建并推送:":"Repo created and pushed: ","创建失败":"Create failed","创建失败:":"Create failed: ","未选择目录":"No folder selected","选择失败:":"Selection failed: ","请输入或选择目录路径":"Enter or pick a folder path","添加目录失败":"Failed to add folder","添加目录失败:":"Failed to add folder: ","删除失败":"Delete failed","删除失败:":"Delete failed: ","强制对齐会用远程分支覆盖本地(丢弃本地未推送的更改与提交),确定继续?":"Force-align overwrites local with the remote branch (drops unpushed local changes and commits). Continue?","已强制对齐到远程分支":"Force-aligned to the remote branch","强制对齐失败":"Force-align failed","强制对齐失败:":"Force-align failed: ","已是 git 仓库":"Already a git repository","已创建 git 仓库,请自行拉取或推送":"Git repository created — pull or push as you wish","创建 git 失败":"Failed to init git","创建 git 失败:":"Failed to init git: ","暂存失败":"Stage failed","暂存失败:":"Stage failed: ","取消暂存失败":"Unstage failed","取消暂存失败:":"Unstage failed: ","已提交 ":"Committed "," 个文件":" file(s)","提交失败":"Commit failed","提交失败:":"Commit failed: ","已提交「":"Committed「","(自动生成)":" (auto-generated)","」并推送":"」 and pushed","已推送本地已有的提交":"Pushed existing local commits","读取分支失败":"Failed to load branches","读取分支失败:":"Failed to load branches: ","切换到分支「":"Switch to branch「","」?(若当前有未提交改动,git 会拒绝切换)":"」? (git will refuse if you have uncommitted changes)","已切换到分支 ":"Switched to branch ","切换分支失败":"Failed to switch branch","切换分支失败:":"Failed to switch branch: ","读取历史失败":"Failed to load history","读取历史失败:":"Failed to load history: ","revert 提交「":"Revert commit「","」——会生成一个反向提交,期间可能产生冲突,确定继续?":"」 — this creates a reverse commit and may cause conflicts. Continue?","已 revert 提交 ":"Reverted commit ","revert 失败":"Revert failed","revert 失败:":"Revert failed: ","cherry-pick 提交「":"Cherry-pick commit「","」到当前分支——期间可能产生冲突,确定继续?":"」 onto the current branch — this may cause conflicts. Continue?","已 cherry-pick 提交 ":"Cherry-picked commit ","cherry-pick 失败":"Cherry-pick failed","cherry-pick 失败:":"Cherry-pick failed: ","读取失败":"Failed to load","已复制":"Copied","短哈希":"short hash","完整哈希":"full hash","提交信息":"commit message","复制失败:请手动复制":"Copy failed — copy it manually","③ 代码管理":"③ Code Management","Gitee 私人令牌(OpenAPI,需 projects 权限)":"Gitee personal token (OpenAPI, requires projects permission)","✅ 已配置":"✅ Configured","(账号 ":" (account ","清除令牌":"Clear token","粘贴 Gitee 私人令牌(https://gitee.com/personal_access_tokens)":"Paste the Gitee personal token (https://gitee.com/personal_access_tokens)","保存令牌":"Save token","令牌只保存在本机 ~/.dsh/storages(0600),不会写进插件目录;需勾选个人令牌的 projects 权限。公钥需已上传到 Gitee(② 里检查)。":"The token is stored only on this machine at ~/.dsh/storages (0600), never in the plugin directory; the personal token must have projects permission. Your public key must be uploaded to Gitee (checked in ②).","选择工作区":"Workspace","选择 DSH 已登记的工作区文件夹":"Choose a workspace folder registered in DSH","(暂无可选工作区)":"(no workspaces available)","— 请选择 —":"— Select —","选择目录":"Choose folder","自定义目录:":"Custom folder: ","删除该目录记录":"Remove this folder entry","删除该目录记录(不删除实际文件夹)":"Remove this folder entry (does not delete the actual folder)","目录":"Folder","分支":"Branch","(无)":"(none)","切换分支":"Switch branch","切换":"Switch","查看提交历史":"View commit history","历史":"History","远程":"Remote","改动":"Changes","无":"None","查看改动的文件列表":"View the list of changed files","查看":"View","同步":"Sync","本地领先 ":"Ahead by "," 提交":" commit(s)","、落后 ":", behind by ","落后 ":"behind by ","与远程一致":"Up to date with remote","查看本地与远程的提交差异":"View commit differences between local and remote","⚠️ 以下 ":"⚠️ The following "," 项 >100MB(超过 GitHub 限制,推送时将自动忽略不上传):":" item(s) >100MB (over GitHub's limit — auto-ignored on push):","/(整个文件夹)":"/ (entire folder)","创建 Git":"Init Git","仅初始化 git 仓库,不拉取不推送,由你决定下一步":"Initializes a git repo only — no pull/push; you decide the next step","本地有改动且远程有更新,可直接用「强制对齐」将本地重置为远程状态":"You have local changes AND remote updates — use \"Force align\" to reset local to the remote state","强制对齐":"Force align","本地完全重置为远程分支(丢弃本地差异),解决“文件相同仍显示同步差异”的情况":"Fully resets local to the remote branch (drops local differences); fixes \"files identical but sync still shows a difference\"","推送更改":"Push changes","拉取更新":"Pull updates","✓ 已是最新":"✓ Up to date","⟳ 正在刷新状态,联网同步 GitHub/Gitee 最新数据…":"⟳ Refreshing status — syncing the latest GitHub/Gitee data…","⟳ 切换平台,正在重新检测「":"⟳ Switching platform, re-checking「","」仓库状态…":"」 repo status…","推送中…":"Pushing…","推送暂存":"Push staged","把已暂存的内容用填写的(或自动生成的)信息提交后推送到远程;不会自动暂存其他未暂存的改动":"Commits only what is staged (with your message, or auto-generated) and pushes it; does not auto-stage other changes","刷新中…":"Refreshing…","刷新状态":"Refresh status","提交信息(留空则用默认)":"Commit message (default if empty)","填写提交信息后点「提交」;留空则自动生成(chore: update <文件夹名>)":"Type a message then click \"Commit\"; leave empty to auto-generate (chore: update <folder>)","提交":"Commit","仓库名称自动取文件夹名(不可修改)":"Repo name is taken from the folder name (read-only)","(未加载文件夹)":"(no folder loaded)","仓库可见性:私有 / 公开":"Repo visibility: private / public","(修改当前仓库的可见性)":" (changes the current repo's visibility)","新建仓库并推送":"Create repo & push","修改仓库状态":"Change repo status","✓ 仓库已是「":"✓ Repo is already「","」状态,如需修改请调整左侧可见性选择。":"」 — to change it, adjust the visibility dropdown.","将把仓库从「":"Will change the repo from「","」改为「":"」 to「","」,点击「修改仓库状态」执行。":"」 — click \"Change repo status\" to apply.","⚠️ 同名仓库已经创建(无法读取当前可见性,可能未":"⚠️ A repo with this name already exists (couldn't read its visibility — maybe not ","配置 Gitee 令牌":"Gitee token configured","登录 gh":"logged into gh","✓ 同名仓库 ":"✓ No repo named "," 不存在,可在 ":" exists — you can create it on ","「新建仓库并推送」创建(可选私有/公开)。":" with \"Create repo & push\" (private or public).","已忽略未上传(":"Skipped uploads ("," 项 >100MB):":" item(s) >100MB):","已提交":"Committed"," 并推送":" and pushed","(推送省略)":" (push skipped)","查看详情":"View details","改动文件":"Changed files","提交历史":"Commit history","与远程同步差异":"Sync differences vs remote","关闭":"Close","已安装 dsh-better-sidebar,此处只列改动文件列表;具体改动内容请到 dsh-better-sidebar 的「源代码管理面板」查看。":"dsh-better-sidebar is installed — this lists changed files only; see the actual changes in its \"Source Control panel\".","点击收起":"Click to collapse","点击查看改动内容":"Click to view the diff","新文件(untracked)暂无内容 diff":"New (untracked) file — no diff yet","已暂存":"Staged","未暂存":"Unstaged","取消暂存":"Unstage","暂存":"Stage","加载中…":"Loading…","▾ 收起":"▾ Collapse","▸ 查看":"▸ View","正在加载改动内容…":"Loading changes…","(无内容差异)":"(no diff)","当前没有改动。":"No changes.","正在读取分支…":"Loading branches…","(无分支)":"(no branches)","(当前)":"(current)","正在读取历史…":"Loading history…","(暂无提交)":"(no commits)","更多操作(右键也可打开)":"More actions (right-click also works)","查看提交差异":"View commit diff","复制短哈希":"Copy short hash","复制完整哈希":"Copy full hash","复制提交信息":"Copy commit message","还原此提交":"Revert this commit","拾取此提交":"Cherry-pick this commit","正在加载提交 diff…":"Loading commit diff…","本地落后 ":"Behind by "," 个提交(远程有而本地没有):":" commit(s) (on remote, not local):"," 个提交(本地有而远程没有):":" commit(s) (on local, not remote):","✓ 已与远程同步,无差异。":"✓ In sync with remote — no differences.","选择代码目录":"Choose a code folder","可手动输入/粘贴目录绝对路径,或点击「浏览…」弹出本地文件夹选择器。确认后将添加到下方下拉并记住,下次打开无需重新选择。":"Paste an absolute folder path, or click \"Browse…\" for the native picker. Confirmed folders are added to the dropdown and remembered.","C:\\Users\\你的用户名\\项目目录":"C:\\Users\\your-name\\project","浏览…":"Browse…","取消":"Cancel","确定":"OK","新增":"Added","删除":"Deleted","重命名":"Renamed","修改":"Modified","旧版本":"Old","新版本":"New","源代码管理":"Source Control","按顺序完成:①环境检查 → ②SSH 密钥与连接 → ③代码管理。推送会自动忽略 >100MB 的文件(":"Work through in order: ① Environment → ② SSH Key & Connection → ③ Code Management. Files over 100MB are auto-ignored on push ("," 限制)并说明原因。":" limit) with the reason shown.","代码管理":"Code Management","拖动调整面板宽度":"Drag to resize the panel","二进制文件,无法查看文本内容":"Binary file — text preview unavailable","(空文件)":"(empty file)"};
|
|
183
183
|
let scmLang = (() => { try { return String(navigator.language || "zh").toLowerCase().split("-")[0] === "en" ? "en" : "zh" } catch { return "zh" } })();
|
|
184
184
|
let scmLocaleFace = null; // ctx.locale (the DSH LocaleRuntime), set in apply()
|
|
185
|
+
let scmSessionList = null; // ctx.get('sessions').list (ObservableSnapshot), set in apply() — 用于判断「无会话空态」
|
|
185
186
|
function currentLang() { try { if (scmLocaleFace && typeof scmLocaleFace.getLocale === "function") return scmLocaleFace.getLocale().active } catch {} return scmLang }
|
|
186
187
|
function t(key, params) {
|
|
187
188
|
let s = key;
|
|
@@ -202,6 +203,18 @@ function useLocale() {
|
|
|
202
203
|
}, [scmLocaleFace]);
|
|
203
204
|
}
|
|
204
205
|
|
|
206
|
+
/** Re-render when the session list changes(新增/切换/清空会话)。用于空态常驻按钮判断:
|
|
207
|
+
* 读到 current === undefined 即「当前没有会话」。 */
|
|
208
|
+
function useSession() {
|
|
209
|
+
const [, force] = useState(0);
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (!scmSessionList || typeof scmSessionList.subscribe !== "function") return;
|
|
212
|
+
const off = scmSessionList.subscribe(() => force((n) => n + 1));
|
|
213
|
+
force((n) => n + 1);
|
|
214
|
+
return off;
|
|
215
|
+
}, [scmSessionList]);
|
|
216
|
+
}
|
|
217
|
+
|
|
205
218
|
|
|
206
219
|
function Field({ label, value, children }) {
|
|
207
220
|
return h("div", { style: { display: "flex", alignItems: "center", gap: 8, marginBottom: 6 } },
|
|
@@ -1269,13 +1282,14 @@ function useLocale() {
|
|
|
1269
1282
|
h("div", { style: { flex: 1, overflow: "auto" } },
|
|
1270
1283
|
detail === "changes" ? h(React.Fragment, null,
|
|
1271
1284
|
hasBetterSidebar ? h("div", { style: { marginBottom: 10, padding: "8px 10px", borderRadius: 8, background: T.layer1, color: T.secondary, fontSize: 12, lineHeight: 1.6 } },
|
|
1272
|
-
t("已安装 dsh-better-sidebar,此处只列改动文件列表;具体改动内容请到 dsh-better-sidebar
|
|
1285
|
+
t("已安装 dsh-better-sidebar,此处只列改动文件列表;具体改动内容请到 dsh-better-sidebar 的「源代码管理面板」查看。"))
|
|
1273
1286
|
: null,
|
|
1274
1287
|
repo && repo.changedFiles && repo.changedFiles.length > 0
|
|
1275
1288
|
? repo.changedFiles.map((f, i) => {
|
|
1276
|
-
// untracked 新文件 git 不跟踪,没有 diff,不可展开。
|
|
1277
1289
|
// 未装 better-sidebar 才能展开 diff(装了它由它的 Git 面板负责,避免重复)。
|
|
1278
|
-
|
|
1290
|
+
// viewable=false(二进制等无法按文本预览)不显示「查看」;
|
|
1291
|
+
// 其余文件(含 untracked 新文件)都可查看内容。
|
|
1292
|
+
const canHaveDiff = !hasBetterSidebar && f.viewable !== false
|
|
1279
1293
|
const open = hasBetterSidebar ? false : expandedDiffs.has(i)
|
|
1280
1294
|
const d = diffs[i] || {}
|
|
1281
1295
|
const onClick = canHaveDiff ? async () => {
|
|
@@ -1311,7 +1325,7 @@ function useLocale() {
|
|
|
1311
1325
|
borderBottom: "1px solid " + T.border, padding: "3px 2px",
|
|
1312
1326
|
cursor: canHaveDiff ? "pointer" : "default",
|
|
1313
1327
|
},
|
|
1314
|
-
title: canHaveDiff ? (open ? t("点击收起") : t("点击查看改动内容")) : t("新文件(untracked)暂无内容 diff"),
|
|
1328
|
+
title: canHaveDiff ? (open ? t("点击收起") : t("点击查看改动内容")) : (f.viewable === false ? t("二进制文件,无法查看文本内容") : t("新文件(untracked)暂无内容 diff")),
|
|
1315
1329
|
onClick: onClick,
|
|
1316
1330
|
},
|
|
1317
1331
|
h("span", { style: { color: typeColor(f.type), fontSize: 11, flexShrink: 0, width: 52 } }, typeLabel(f.type)),
|
|
@@ -1333,7 +1347,9 @@ function useLocale() {
|
|
|
1333
1347
|
open && canHaveDiff ? (
|
|
1334
1348
|
d.loading ? h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, t("正在加载改动内容…"))
|
|
1335
1349
|
: d.error ? h("div", { style: { fontSize: 12, color: T.danger, padding: "4px 8px" } }, "❌ " + d.error)
|
|
1336
|
-
: d.diff ? renderSideBySideDiff(d.diff)
|
|
1350
|
+
: d.diff ? renderSideBySideDiff(d.diff)
|
|
1351
|
+
: f.type === 'untracked' ? h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, t("(空文件)"))
|
|
1352
|
+
: h("div", { style: { fontSize: 12, color: T.secondary, padding: "4px 8px" } }, t("(无内容差异)"))
|
|
1337
1353
|
) : null
|
|
1338
1354
|
);
|
|
1339
1355
|
})
|
|
@@ -1563,22 +1579,25 @@ function useLocale() {
|
|
|
1563
1579
|
return rows;
|
|
1564
1580
|
}
|
|
1565
1581
|
|
|
1566
|
-
/** 并排 diff 渲染器:删除行红底居左、新增行绿底居右,上下文两列相同。
|
|
1582
|
+
/** 并排 diff 渲染器:删除行红底居左、新增行绿底居右,上下文两列相同。
|
|
1583
|
+
* 没有删除行的 diff(新增/untracked 文件等,无旧版本)只显示「新版本」一列。 */
|
|
1567
1584
|
function renderSideBySideDiff(diff) {
|
|
1568
1585
|
const rows = parseUnifiedDiff(diff);
|
|
1569
1586
|
if (rows.length === 0) return renderDiff(diff);
|
|
1570
1587
|
const mono = { fontFamily: "var(--ds-font-family-code, ui-monospace, monospace)", fontSize: 12, lineHeight: 1.55, whiteSpace: "pre-wrap", wordBreak: "break-all" };
|
|
1571
1588
|
const cellBg = { del: "rgba(239,68,68,.12)", add: "rgba(34,197,94,.12)", ctx: "transparent" };
|
|
1572
1589
|
const gutter = { ...mono, fontSize: 10, lineHeight: 1.55, color: T.secondary, textAlign: "right", padding: "0 6px", whiteSpace: "nowrap", wordBreak: "normal", opacity: 0.7 };
|
|
1590
|
+
// 新增文件没有旧版本:diff 中没有任何删除行时,隐藏「旧版本」列。
|
|
1591
|
+
const newOnly = !rows.some((r) => r.type === "del");
|
|
1573
1592
|
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)" } },
|
|
1574
1593
|
h("div", { style: { display: "flex", borderBottom: "1px solid " + T.border, position: "sticky", top: 0, background: "var(--dsw-alias-bg-layer-3, #fff)", zIndex: 1 } },
|
|
1575
|
-
h("div", { style: { flex: 1, padding: "3px 8px", fontSize: 12, fontWeight: 600, color: T.danger } }, t("旧版本")),
|
|
1594
|
+
newOnly ? null : h("div", { style: { flex: 1, padding: "3px 8px", fontSize: 12, fontWeight: 600, color: T.danger } }, t("旧版本")),
|
|
1576
1595
|
h("div", { style: { flex: 1, padding: "3px 8px", fontSize: 12, fontWeight: 600, color: T.success } }, t("新版本"))
|
|
1577
1596
|
),
|
|
1578
1597
|
rows.map((r, i) =>
|
|
1579
1598
|
h("div", { key: i, style: { display: "flex", background: cellBg[r.type] || "transparent", borderBottom: r.type === "ctx" ? "none" : "1px solid rgba(128,128,128,.1)" } },
|
|
1580
|
-
h("div", { style: { width: 34, flexShrink: 0, ...gutter } }, r.oldNo || ""),
|
|
1581
|
-
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 || " ")),
|
|
1599
|
+
newOnly ? null : h("div", { style: { width: 34, flexShrink: 0, ...gutter } }, r.oldNo || ""),
|
|
1600
|
+
newOnly ? null : 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 || " ")),
|
|
1582
1601
|
h("div", { style: { width: 34, flexShrink: 0, ...gutter } }, r.newNo || ""),
|
|
1583
1602
|
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 || " "))
|
|
1584
1603
|
)
|
|
@@ -1778,6 +1797,25 @@ function useLocale() {
|
|
|
1778
1797
|
);
|
|
1779
1798
|
}
|
|
1780
1799
|
|
|
1800
|
+
/** 空态(无会话)时的常驻右上角入口:注册进常驻的 shell.overlay 槽,用 fixed 钉在
|
|
1801
|
+
* 视口右上角。仅当「当前没有会话」(sessionList.current === undefined)时显示——
|
|
1802
|
+
* 有会话时交给 header utilities 按钮,避免两处重复。点击复用 HeaderScmAction 打开面板。 */
|
|
1803
|
+
function EmptyStateScmAction() {
|
|
1804
|
+
useSession();
|
|
1805
|
+
const snap = scmSessionList && typeof scmSessionList.getSnapshot === "function" ? scmSessionList.getSnapshot() : undefined;
|
|
1806
|
+
// 尚未捕获到 sessions 时先不渲染:避免刷新瞬间(captureSessions 的 1.2s 延迟窗口内)
|
|
1807
|
+
// 因 scmSessionList 为 null 而错误点亮常驻按钮、与「Session 日志」重叠。
|
|
1808
|
+
if (!snap) return null;
|
|
1809
|
+
const current = snap.current;
|
|
1810
|
+
const cur = current !== undefined ? (snap.byId ? snap.byId[current] : undefined) : undefined;
|
|
1811
|
+
// 活跃(非空白)会话 → header 里已有「代码管理」按钮(会话内);这里隐藏常驻按钮。
|
|
1812
|
+
// 空白(新对话/欢迎)或根本没有会话 → header 被 hideChrome 隐藏/不存在,显示常驻右上角按钮。
|
|
1813
|
+
const hasActiveSession = current !== undefined && cur && cur.blank === false;
|
|
1814
|
+
if (hasActiveSession) return null;
|
|
1815
|
+
return h("div", { style: { position: "fixed", top: 8, right: 12, zIndex: 40, pointerEvents: "auto" } },
|
|
1816
|
+
h(HeaderScmAction));
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1781
1819
|
/** 挂载一个经典脚本 React 根(React 18 用 createRoot,老版本回退 render)。 */
|
|
1782
1820
|
function mountClientRoot(container, element) {
|
|
1783
1821
|
try {
|
|
@@ -1833,6 +1871,23 @@ function useLocale() {
|
|
|
1833
1871
|
}, 1200);
|
|
1834
1872
|
}
|
|
1835
1873
|
|
|
1874
|
+
// 捕获 `sessions.list`(用于「是否无会话」判断)。sessions 是核心服务、通常早于本插件
|
|
1875
|
+
// 就绪;若还没就绪,单次延迟补抓一次(组件经 useSession 订阅后会自动重渲染)。
|
|
1876
|
+
const captureSessions = () => {
|
|
1877
|
+
try {
|
|
1878
|
+
if (scmSessionList) return true;
|
|
1879
|
+
const s = ctx && typeof ctx.get === "function" ? ctx.get("sessions") : undefined;
|
|
1880
|
+
if (s && typeof s.list === "object" && s.list) {
|
|
1881
|
+
scmSessionList = s.list;
|
|
1882
|
+
return true;
|
|
1883
|
+
}
|
|
1884
|
+
return false;
|
|
1885
|
+
} catch { return false }
|
|
1886
|
+
};
|
|
1887
|
+
if (!captureSessions()) {
|
|
1888
|
+
window.setTimeout(() => { captureSessions(); }, 1200);
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1836
1891
|
// DSH 打开(插件激活)时就预取环境/SSH/默认工作区/仓库状态,
|
|
1837
1892
|
// 点开「代码管理」面板时直接使用缓存,无需重新加载。
|
|
1838
1893
|
void preload();
|
|
@@ -1844,7 +1899,9 @@ function useLocale() {
|
|
|
1844
1899
|
|
|
1845
1900
|
const tryRegisterTab = () => {
|
|
1846
1901
|
const bs = typeof ctx.get === "function" ? ctx.get("betterSidebar") : undefined;
|
|
1847
|
-
if (!bs || typeof bs.registerTab !== "function")
|
|
1902
|
+
if (!bs || typeof bs.registerTab !== "function") {
|
|
1903
|
+
return false;
|
|
1904
|
+
}
|
|
1848
1905
|
// 分支 A:已安装 dsh-better-sidebar —— 把「代码管理」注册成它的侧边栏新 Tab
|
|
1849
1906
|
// 页面。ctx.effect 保证 HMR / 插件卸载时自动注销该 Tab。
|
|
1850
1907
|
hasBetterSidebar = true;
|
|
@@ -1880,17 +1937,38 @@ function useLocale() {
|
|
|
1880
1937
|
|
|
1881
1938
|
if (tryRegisterTab()) return;
|
|
1882
1939
|
|
|
1883
|
-
// 分支 B:此刻未检测到 dsh-better-sidebar ——
|
|
1884
|
-
// conversation.session.header.utilities
|
|
1885
|
-
//
|
|
1886
|
-
//
|
|
1940
|
+
// 分支 B:此刻未检测到 dsh-better-sidebar —— 注册两处「代码管理」入口,保证右上角常驻可见:
|
|
1941
|
+
// ① 有会话时:放进 conversation.session.header.utilities(Session 日志旁的右对齐列表);
|
|
1942
|
+
// ② 无会话空态时:放进常驻的 shell.overlay 槽、用 fixed 钉在右上角(EmptyStateScmAction
|
|
1943
|
+
// 内部按「是否有当前会话」决定显隐,有会话时返回 null,避免与 ① 重复)。
|
|
1944
|
+
// 点击都复用 HeaderScmAction 打开右侧 dsh-better-sidebar 外观的集成面板(推挤 #root)。
|
|
1887
1945
|
const slots = typeof ctx.get === "function" ? ctx.get("slots") : undefined;
|
|
1888
1946
|
if (slots && typeof slots.inject === "function") {
|
|
1889
|
-
ctx.effect(() =>
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1947
|
+
ctx.effect(() => {
|
|
1948
|
+
const teardowns = [];
|
|
1949
|
+
// ① 会话内:Session 日志旁
|
|
1950
|
+
teardowns.push(slots.inject('conversation.session.header.utilities', () => slots.register({
|
|
1951
|
+
name: 'conversation.session.header.utilities',
|
|
1952
|
+
id: 'source-code-mgmt',
|
|
1953
|
+
order: 200,
|
|
1954
|
+
}, HeaderScmAction)));
|
|
1955
|
+
// ② 空态:常驻右上角(仅无会话时显示)
|
|
1956
|
+
teardowns.push(slots.inject('shell.overlay', () => slots.register({
|
|
1957
|
+
name: 'shell.overlay',
|
|
1958
|
+
id: 'source-code-mgmt-empty',
|
|
1959
|
+
order: 300,
|
|
1960
|
+
}, EmptyStateScmAction)));
|
|
1961
|
+
// entryUnmount 一次性拆掉两处:这样当之后探测到 better-sidebar、切到分支 A
|
|
1962
|
+
// (注册 Tab)时,tryRegisterTab 里的 entryUnmount() 能把两处入口都移除。
|
|
1963
|
+
// (旧代码只给 ReactDOM 降级路径设了 entryUnmount,slots 路径漏了 —— 这正是
|
|
1964
|
+
// 「better-sidebar 已出来、代码管理按钮却仍在」的早期根因。)
|
|
1965
|
+
entryUnmount = () => {
|
|
1966
|
+
for (const td of teardowns) { try { if (typeof td === "function") td() } catch {} }
|
|
1967
|
+
teardowns.length = 0;
|
|
1968
|
+
entryUnmount = null;
|
|
1969
|
+
};
|
|
1970
|
+
return entryUnmount;
|
|
1971
|
+
}, 'source-code-mgmt: entry (session header + empty overlay)');
|
|
1894
1972
|
} else if (ReactDOM) {
|
|
1895
1973
|
// slots 服务不可用(极少数环境)——降级:右上角浮动按钮 + 右侧面板。
|
|
1896
1974
|
ctx.effect(() => {
|
|
@@ -1906,10 +1984,26 @@ function useLocale() {
|
|
|
1906
1984
|
});
|
|
1907
1985
|
}
|
|
1908
1986
|
|
|
1909
|
-
//
|
|
1910
|
-
//
|
|
1911
|
-
|
|
1912
|
-
|
|
1987
|
+
// 有界重试:作为安全网,兜住 better-sidebar 的客户端服务晚于本插件挂载的情况
|
|
1988
|
+
// (两插件互不注入、加载器不保证顺序;其客户端 bundle 是重插件,偶发冷启动慢)。
|
|
1989
|
+
// 单次 1.5s 窗口不够,这里先用快节奏起步、再放慢,覆盖到约 44s;一旦拿到服务
|
|
1990
|
+
// (tryRegisterTab 返回 true)立即注册 Tab、拆除 header 入口并清空所有定时器;
|
|
1991
|
+
// 全程零 I/O(纯内存 ctx.get)。窗口耗尽仍无服务则停留分支 B(此时才是真正未安装)。
|
|
1992
|
+
const retryTimers = new Set();
|
|
1993
|
+
const retryTicks = [500, 500, 750, 1000, 1500, 2000, 3000, 5000, 5000, 5000, 5000, 5000, 5000, 5000];
|
|
1994
|
+
const clearRetry = () => {
|
|
1995
|
+
for (const timerId of retryTimers) { try { window.clearTimeout(timerId); } catch {} }
|
|
1996
|
+
retryTimers.clear();
|
|
1997
|
+
};
|
|
1998
|
+
for (const d of retryTicks) {
|
|
1999
|
+
const handle = window.setTimeout(() => {
|
|
2000
|
+
retryTimers.delete(handle);
|
|
2001
|
+
if (hasBetterSidebar) return;
|
|
2002
|
+
if (tryRegisterTab()) clearRetry();
|
|
2003
|
+
}, d);
|
|
2004
|
+
retryTimers.add(handle);
|
|
2005
|
+
}
|
|
2006
|
+
ctx.effect(() => clearRetry);
|
|
1913
2007
|
}
|
|
1914
2008
|
|
|
1915
2009
|
exports.name = PLUGIN_ID;
|
package/lib/index.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import { execFileSync, spawn } from 'node:child_process'
|
|
20
20
|
import {
|
|
21
21
|
existsSync, mkdirSync, writeFileSync, readFileSync, chmodSync,
|
|
22
|
-
readdirSync, statSync,
|
|
22
|
+
readdirSync, statSync, openSync, readSync, closeSync,
|
|
23
23
|
} from 'node:fs'
|
|
24
24
|
import { join, dirname } from 'node:path'
|
|
25
25
|
import { homedir } from 'node:os'
|
|
@@ -299,8 +299,14 @@ function run(cmd, args, opts = {}) {
|
|
|
299
299
|
if (cmd === GIT && !env.GIT_SSH && WORKING_SSH) {
|
|
300
300
|
env.GIT_SSH = WORKING_SSH
|
|
301
301
|
}
|
|
302
|
+
// core.quotepath 默认开启:git 会把含非 ASCII 字节的路径输出成带八进制转义
|
|
303
|
+
// 的引号形式(如 "\346\270\270\346\210\217\346\250\241\345\274\217…md"),
|
|
304
|
+
// 中文文件名在改动列表 / diff 里就会显示成乱码。统一用
|
|
305
|
+
// `-c core.quotepath=false` 让 git 直接输出原始 UTF-8 路径,Node 以 utf8
|
|
306
|
+
// 解码后即得到正确的中文文件名(git diff / git show 头部路径也一并修复)。
|
|
307
|
+
const finalArgs = cmd === GIT ? ['-c', 'core.quotepath=false', ...args] : args
|
|
302
308
|
try {
|
|
303
|
-
const out = execFileSync(cmd,
|
|
309
|
+
const out = execFileSync(cmd, finalArgs, {
|
|
304
310
|
encoding: 'utf8',
|
|
305
311
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
306
312
|
timeout: opts.timeout ?? 30_000,
|
|
@@ -317,6 +323,46 @@ function run(cmd, args, opts = {}) {
|
|
|
317
323
|
}
|
|
318
324
|
}
|
|
319
325
|
|
|
326
|
+
/**
|
|
327
|
+
* Decode a path as printed by git. With core.quotepath ON (the default) git
|
|
328
|
+
* prints paths containing non-ASCII bytes / special characters as a
|
|
329
|
+
* double-quoted C-style string with octal escapes, e.g.
|
|
330
|
+
* `"\346\270\270\346\210\217\346\250\241\345\274\217Agent\351\242\204\350\256\276.md"`
|
|
331
|
+
* for `游戏模式Agent预设.md`. This unescapes that form back into the real
|
|
332
|
+
* UTF-8 path. Paths that are not quoted (already raw UTF-8, e.g. when
|
|
333
|
+
* core.quotepath=false) pass through unchanged.
|
|
334
|
+
* @param {string} raw - path fragment as printed by git.
|
|
335
|
+
* @returns {string} decoded path.
|
|
336
|
+
*/
|
|
337
|
+
function parseGitPath(raw) {
|
|
338
|
+
const s = String(raw ?? '')
|
|
339
|
+
if (s.length < 2 || s[0] !== '"' || s[s.length - 1] !== '"') return s
|
|
340
|
+
const inner = s.slice(1, -1)
|
|
341
|
+
const bytes = []
|
|
342
|
+
for (let i = 0; i < inner.length; i++) {
|
|
343
|
+
const ch = inner[i]
|
|
344
|
+
if (ch !== '\\') { bytes.push(inner.charCodeAt(i)); continue }
|
|
345
|
+
const nxt = inner[i + 1]
|
|
346
|
+
if (nxt >= '0' && nxt <= '7') {
|
|
347
|
+
let v = 0
|
|
348
|
+
let n = 0
|
|
349
|
+
while (n < 3 && i + 1 + n < inner.length) {
|
|
350
|
+
const d = inner.charCodeAt(i + 1 + n) - 48
|
|
351
|
+
if (d < 0 || d > 7) break
|
|
352
|
+
v = v * 8 + d
|
|
353
|
+
n++
|
|
354
|
+
}
|
|
355
|
+
bytes.push(v)
|
|
356
|
+
i += n
|
|
357
|
+
continue
|
|
358
|
+
}
|
|
359
|
+
const esc = { a: 0x07, b: 0x08, t: 0x09, n: 0x0a, v: 0x0b, f: 0x0c, r: 0x0d, '"': 0x22, '\\': 0x5c }[nxt]
|
|
360
|
+
if (esc !== undefined) { bytes.push(esc); i++; continue }
|
|
361
|
+
bytes.push(inner.charCodeAt(i))
|
|
362
|
+
}
|
|
363
|
+
return Buffer.from(bytes).toString('utf8')
|
|
364
|
+
}
|
|
365
|
+
|
|
320
366
|
/** Check a tool (git/gh) — version when installed. */
|
|
321
367
|
function toolStatus(bin, args) {
|
|
322
368
|
const r = run(bin, args)
|
|
@@ -729,6 +775,62 @@ function providerRemoteName(dir, provider) {
|
|
|
729
775
|
return matched
|
|
730
776
|
}
|
|
731
777
|
|
|
778
|
+
/**
|
|
779
|
+
* Read a file for preview purposes, capped at `maxBytes` so a huge untracked
|
|
780
|
+
* file never gets slurped into memory whole. Returns { buf, truncated }.
|
|
781
|
+
* @param {string} full - absolute path.
|
|
782
|
+
* @param {number} [maxBytes] - cap, default 1MB.
|
|
783
|
+
*/
|
|
784
|
+
function readFilePreview(full, maxBytes = 1024 * 1024) {
|
|
785
|
+
const st = statSync(full)
|
|
786
|
+
if (st.size <= maxBytes) return { buf: readFileSync(full), truncated: false }
|
|
787
|
+
const fd = openSync(full, 'r')
|
|
788
|
+
try {
|
|
789
|
+
const buf = Buffer.alloc(maxBytes)
|
|
790
|
+
let off = 0
|
|
791
|
+
while (off < maxBytes) {
|
|
792
|
+
const n = readSync(fd, buf, off, maxBytes - off, off)
|
|
793
|
+
if (n <= 0) break
|
|
794
|
+
off += n
|
|
795
|
+
}
|
|
796
|
+
return { buf: buf.subarray(0, off), truncated: true }
|
|
797
|
+
} finally {
|
|
798
|
+
closeSync(fd)
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Whether a changed file's content can be shown as text in the panel
|
|
804
|
+
* (binary files cannot). 决定该文件是否显示「查看」:
|
|
805
|
+
* - 工作区里存在的文件:直接读取开头 8KB 嗅探(与 git 相同的 NUL 字节启发式);
|
|
806
|
+
* - 已删除 / 工作区副本不存在的文件:用 `git diff --numstat` 的二进制标记判断
|
|
807
|
+
* (二进制条目输出 "-\t-")。
|
|
808
|
+
* @param {string} dir - repo root.
|
|
809
|
+
* @param {string} path - repo-relative path.
|
|
810
|
+
* @param {string} type - changedFiles 条目类型(untracked/added/deleted/renamed/modified)。
|
|
811
|
+
* @returns {boolean} 内容是否可以按文本预览。
|
|
812
|
+
*/
|
|
813
|
+
function fileViewable(dir, path, type) {
|
|
814
|
+
const p = String(path || '')
|
|
815
|
+
if (p === '') return false
|
|
816
|
+
try {
|
|
817
|
+
const fd = openSync(join(dir, p), 'r')
|
|
818
|
+
try {
|
|
819
|
+
const buf = Buffer.alloc(8192)
|
|
820
|
+
const n = readSync(fd, buf, 0, buf.length, 0)
|
|
821
|
+
return !buf.subarray(0, n).includes(0)
|
|
822
|
+
} finally {
|
|
823
|
+
closeSync(fd)
|
|
824
|
+
}
|
|
825
|
+
} catch {
|
|
826
|
+
// 工作区副本缺失(已删除、或暂存新增后又删掉):交给 git 判断二进制。
|
|
827
|
+
const cached = type === 'added' || type === 'renamed'
|
|
828
|
+
const r = run(GIT, ['-C', dir, 'diff', ...(cached ? ['--cached', '--numstat'] : ['--numstat']), '--', p])
|
|
829
|
+
const line = (r.stdout || '').split(/\r?\n/).find((l) => l.trim() !== '')
|
|
830
|
+
return line ? !line.startsWith('-\t-') : false
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
|
|
732
834
|
/**
|
|
733
835
|
* Repo status for a selected folder: whether it is a git repo, remote,
|
|
734
836
|
* branch, dirty count, tracked-over-100MB files, and new >100MB candidates.
|
|
@@ -771,10 +873,9 @@ function repoStatus(dir, provider = 'github', full = false) {
|
|
|
771
873
|
const arrow = path.indexOf(' -> ')
|
|
772
874
|
if (arrow !== -1) path = path.slice(arrow + 4)
|
|
773
875
|
}
|
|
774
|
-
// Git may quote paths with special characters
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
}
|
|
876
|
+
// Git may quote paths with special characters (core.quotepath) as
|
|
877
|
+
// C-style "…" strings with octal escapes; decode them back to UTF-8.
|
|
878
|
+
path = parseGitPath(path)
|
|
778
879
|
const type = /^\?\?/.test(code) ? 'untracked'
|
|
779
880
|
: /^A|^AM/.test(code) ? 'added'
|
|
780
881
|
: /^D|^AD/.test(code) ? 'deleted'
|
|
@@ -784,14 +885,15 @@ function repoStatus(dir, provider = 'github', full = false) {
|
|
|
784
885
|
const staged = code[0] !== ' ' && code[0] !== '?'
|
|
785
886
|
// diff 内容不再在 /repo 里逐个生成(改动多时会跑 N 次 git diff,拖慢加载);
|
|
786
887
|
// 改为点开「查看改动」弹窗里的文件行时,由 /repo-diff 单独按需请求。
|
|
787
|
-
|
|
888
|
+
// viewable=false 表示二进制等无法按文本预览的内容(列表里不显示「查看」)。
|
|
889
|
+
return { type, path, staged, viewable: fileViewable(dir, path, type) }
|
|
788
890
|
})
|
|
789
891
|
|
|
790
892
|
// Tracked files that exceed 100MB (GitHub would reject a push of these).
|
|
791
893
|
let trackedOverLimit = []
|
|
792
894
|
const lsr = run(GIT, ['-C', dir, 'ls-files', '-z'])
|
|
793
895
|
if (lsr.ok) {
|
|
794
|
-
const tracked = lsr.stdout.split('\0').filter(Boolean)
|
|
896
|
+
const tracked = lsr.stdout.split('\0').filter(Boolean).map(parseGitPath)
|
|
795
897
|
for (const p of tracked) {
|
|
796
898
|
const full = join(dir, p)
|
|
797
899
|
try {
|
|
@@ -952,7 +1054,7 @@ function pushFlow(dir) {
|
|
|
952
1054
|
|
|
953
1055
|
// What got staged? Report for transparency.
|
|
954
1056
|
const staged = run(GIT, ['-C', dir, 'diff', '--cached', '--name-only'])
|
|
955
|
-
const stagedFiles = staged.ok ? staged.stdout.split(/\r?\n/).filter(Boolean) : []
|
|
1057
|
+
const stagedFiles = staged.ok ? staged.stdout.split(/\r?\n/).filter(Boolean).map(parseGitPath) : []
|
|
956
1058
|
|
|
957
1059
|
let committed = false
|
|
958
1060
|
let commitHash
|
|
@@ -1248,8 +1350,9 @@ function fmtMB(bytes) {
|
|
|
1248
1350
|
* Return the diff of a single changed file, on demand (the "查看改动" panel
|
|
1249
1351
|
* expands a file row and fetches this instead of bundling every diff into
|
|
1250
1352
|
* /repo, which would spawn N git processes and stall the load on large
|
|
1251
|
-
* change sets). untracked files have no
|
|
1252
|
-
*
|
|
1353
|
+
* change sets). untracked files have no git history, so their whole content
|
|
1354
|
+
* is returned as an "all added" diff so the panel can show it; binary /
|
|
1355
|
+
* unreadable files return viewable:false (the list then hides 查看).
|
|
1253
1356
|
* @param {string} dir - local folder.
|
|
1254
1357
|
* @param {string} path - repo-relative file path.
|
|
1255
1358
|
*/
|
|
@@ -1262,7 +1365,32 @@ function repoDiffFlow(dir, path) {
|
|
|
1262
1365
|
// Untracked files have no tracked diff.
|
|
1263
1366
|
const status = run(GIT, ['-C', dir, 'status', '--porcelain', '--', p])
|
|
1264
1367
|
const isUntracked = status.ok && /^\?\?/.test(status.stdout.trim().slice(0, 2))
|
|
1265
|
-
if (isUntracked)
|
|
1368
|
+
if (isUntracked) {
|
|
1369
|
+
// 新文件没有历史版本可对比:读取工作区内容,拼成「全部新增」的统一 diff,
|
|
1370
|
+
// 面板就能用现有的并排 diff 渲染器展示完整内容。二进制/不可读文件返回
|
|
1371
|
+
// viewable:false(列表里不显示「查看」)。
|
|
1372
|
+
const full = join(dir, p)
|
|
1373
|
+
let preview
|
|
1374
|
+
try { preview = readFilePreview(full) } catch { return { ok: true, path: p, diff: '', viewable: false } }
|
|
1375
|
+
if (preview.buf.includes(0)) return { ok: true, path: p, diff: '', viewable: false }
|
|
1376
|
+
const content = preview.buf.toString('utf8')
|
|
1377
|
+
const overSize = preview.truncated
|
|
1378
|
+
const noFinalNl = !overSize && !/[\r\n]$/.test(content)
|
|
1379
|
+
const lines = content.replace(/\r\n/g, '\n').split('\n')
|
|
1380
|
+
if (lines.length > 0 && lines[lines.length - 1] === '') lines.pop()
|
|
1381
|
+
if (lines.length === 0) return { ok: true, path: p, diff: '', viewable: true }
|
|
1382
|
+
// 内容过长时只展示前 2000 行,避免面板渲染卡顿。
|
|
1383
|
+
const shown = lines.slice(0, 2000)
|
|
1384
|
+
const truncated = overSize || lines.length > shown.length
|
|
1385
|
+
let body = shown.map((l) => '+' + l).join('\n')
|
|
1386
|
+
if (truncated) body += '\n+…(内容较长,仅显示前 ' + shown.length + ' 行)'
|
|
1387
|
+
if (noFinalNl) body += '\n\'
|
|
1388
|
+
const head = 'diff --git a/' + p + ' b/' + p + '\n' +
|
|
1389
|
+
'--- /dev/null\n' +
|
|
1390
|
+
'+++ b/' + p + '\n' +
|
|
1391
|
+
'@@ -0,0 +1,' + shown.length + ' @@\n'
|
|
1392
|
+
return { ok: true, path: p, viewable: true, diff: head + body + '\n' }
|
|
1393
|
+
}
|
|
1266
1394
|
let r = run(GIT, ['-C', dir, 'diff', '--', p])
|
|
1267
1395
|
if (r.ok && !r.stdout.trim()) {
|
|
1268
1396
|
r = run(GIT, ['-C', dir, 'diff', '--cached', '--', p])
|
|
@@ -1396,7 +1524,7 @@ function commitFlow(dir, message, paths) {
|
|
|
1396
1524
|
} else {
|
|
1397
1525
|
// 提交整个暂存区:列出已暂存文件用于提示。
|
|
1398
1526
|
const st = run(GIT, ['-C', dir, 'diff', '--cached', '--name-only'])
|
|
1399
|
-
stagedPaths = st.ok ? st.stdout.split(/\r?\n/).filter(Boolean) : []
|
|
1527
|
+
stagedPaths = st.ok ? st.stdout.split(/\r?\n/).filter(Boolean).map(parseGitPath) : []
|
|
1400
1528
|
}
|
|
1401
1529
|
|
|
1402
1530
|
// 无可提交内容时避免报错。
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "source-code-mgmt",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.11.0",
|
|
4
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
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|