@feng0531/dsh-vcs 0.3.67 → 0.3.69
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -46
- package/THIRD_PARTY_NOTICES.md +3 -1
- package/dist/client.js +3 -3
- package/dist/editor.js +87 -87
- package/docs/RELEASING.md +31 -44
- package/index.mjs +6 -5
- package/package.json +1 -1
- package/text-content.mjs +24 -0
- package/vcs.mjs +23 -10
package/docs/RELEASING.md
CHANGED
|
@@ -1,65 +1,52 @@
|
|
|
1
|
-
#
|
|
1
|
+
# 维护者发布流程
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
本文用于维护项目。使用者请阅读[README](<../README.md>)和[安装指南](https://github.com/f-e-n-g-0531/dsh-vcs/blob/main/docs/INSTALL.md)。
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## 发布内容
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
每个新版本必须有独立的中文发布说明,放在 `docs/releases/版本号.md`。按“本版变化、安装、限制、验证”组织,不把自动提交列表、开发轮次或内部恢复记录当作用户说明。没有新增功能时明确写出,不夸大改动。
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
更新包版本和锁文件,在变更记录中填写同一版本。核查许可证、第三方声明及公开文件,排除凭据和私人数据。
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
## 1. 本地检查
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
需要 Node.js 22+、Git、SVN、svnadmin。在干净检出中执行:
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
- [ ] 审查待公开文件和 Git 历史,排除个人路径、令牌、用户数据、私人记录和敏感图片。
|
|
15
|
+
npm ci --include=dev
|
|
16
|
+
npm run check
|
|
17
|
+
npm run verify:package
|
|
18
|
+
npm run test:install
|
|
20
19
|
|
|
21
|
-
|
|
20
|
+
测试失败或跳过须调查;隔离安装只检查包和宿主注册,不是用户实际 DSH 验收。
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
## 2. 固定版本并验证
|
|
24
23
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
npm pack
|
|
24
|
+
1. 提交并推送版本修改,创建新标签 `vX.Y.Z`;不移动已有标签。
|
|
25
|
+
2. 等待标签 CI 的 Windows/Ubuntu × Node 22/24 全部成功。
|
|
26
|
+
3. 手动在同一标签运行 **Browser layout checks**,等待成功。
|
|
27
|
+
4. 下载四个 CI 候选 tgz,确认字节大小和 SHA-256 全部一致。
|
|
30
28
|
|
|
31
|
-
|
|
29
|
+
分支检查不能代替同标签检查。失败后先定位;需要修改源码或测试时使用新的版本和标签。
|
|
32
30
|
|
|
33
|
-
|
|
31
|
+
## 3. 联合发布
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
在 Actions 从 main 运行 **Publish GitHub and npm**,输入已验证的标签。
|
|
36
34
|
|
|
37
|
-
|
|
35
|
+
工作流检出该标签,重新检查并打包。它先上传 npm,再验证公开 npm 元数据与实际下载包,最后创建 GitHub 正式 Release,上传同一个 tgz 和 SHA-256 校验文件。
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
仓库需配置 `NPM_AUTOMATION_TOKEN`,具有该包发布权限并符合 npm 2FA 策略。凭据只用于上传步骤;不要写进源码、发布说明或聊天。
|
|
40
38
|
|
|
41
|
-
|
|
39
|
+
## 4. 发布后核验
|
|
42
40
|
|
|
43
|
-
|
|
41
|
+
- npm 版本与 latest 正确,实际 tarball 可下载。
|
|
42
|
+
- GitHub Release 非草稿、非预发布,附件可下载。
|
|
43
|
+
- 两端实际包与四候选的字节大小和 SHA-256 一致。
|
|
44
|
+
- 补记最终结论,但不修改已发布标签或替换工件。
|
|
44
45
|
|
|
45
|
-
|
|
46
|
-
- [ ] 当前 / 上级仓库、一层子目录发现,以及明确相对子目录的定向发现。
|
|
47
|
-
- [ ] Git 三模式、SVN BASE 与属性;新增、删除、重命名、未跟踪和冲突提示。
|
|
48
|
-
- [ ] UTF-8、GBK、带 BOM 的 UTF-16;二进制与超大文件提示。
|
|
49
|
-
- [ ] 并排 / 行内、空白选项、换行、导航、主题和编辑器加载失败降级。
|
|
50
|
-
- [ ] Session / 仓库切换与重新发现不展示过期内容;预览不修改工作区或暂存区。
|
|
51
|
-
- [ ] 发现授权过期后的手动重新发现,以及取消请求的实际反馈。
|
|
46
|
+
npm 与 GitHub 不是原子发布。若 npm 已接受上传但公开下载暂时 404,**不要再次上传**;先只读确认公开包。确认相同后恢复原失败任务,工作流会核对已有 npm integrity 并跳过上传。不匹配则停止;已有 GitHub Release 不覆盖。
|
|
52
47
|
|
|
53
|
-
|
|
48
|
+
## 实机验证与安装边界
|
|
54
49
|
|
|
55
|
-
|
|
50
|
+
自动浏览器测试不等于当前 DSH 实机通过。发布说明须如实写出未确认项;用户明确授权在此边界下发布时,不伪造实机结论。
|
|
56
51
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
CI 会严格校验标签为 `vX.Y.Z` 且与包版本相同。联合发布检查同标签同提交的最新四组合CI和Chrome工作流成功,并下载四候选包比较字节与SHA-256;未运行、失败、尚在运行或包不一致时拒绝发布。公开npm元数据、实际tarball及latest也须验证,再上传同包至GitHub。这不替代真实 DSH 页面人工验收;用户授权发布未实机验收功能时,Release必须明确披露,不自动安装或重启。
|
|
60
|
-
|
|
61
|
-
## 5. 分发
|
|
62
|
-
|
|
63
|
-
确认完整附带许可证及第三方声明,不把本项目标记为标准开源软件。在已确认的仓库中按实际验证结果编写发布说明,附审核过的 tgz 和校验值,注明兼容环境与未验证事项。是否打 tag / 创建 GitHub Release 由维护者决定;不为本次整理虚构日期、历史版本或功能变更。
|
|
64
|
-
|
|
65
|
-
相关约定见 [README](<../README.md>)、[贡献指南](<https://github.com/f-e-n-g-0531/dsh-vcs/blob/main/CONTRIBUTING.md>)、[变更记录](<https://github.com/f-e-n-g-0531/dsh-vcs/blob/main/CHANGELOG.md>) 和 [安全说明](<https://github.com/f-e-n-g-0531/dsh-vcs/blob/main/SECURITY.md>)。
|
|
52
|
+
发布不安装、不升级、不重启运行中的 DSH。用户选择安装后,在原 Web 页面检查入口、仓库发现、差异、历史以及 Session 切换;由用户自行重启。维护者不能启动替代服务器冒充当前页面。
|
package/index.mjs
CHANGED
|
@@ -34,11 +34,11 @@ const REQUEST_FIELDS = {
|
|
|
34
34
|
'vcs/compare': ['sessionId', 'repositoryId', 'mode', 'id'],
|
|
35
35
|
'vcs/history': ['sessionId', 'repositoryId', 'snapshot', 'offset', 'limit', 'search'],
|
|
36
36
|
'vcs/commit': ['sessionId', 'repositoryId', 'commit', 'parentIndex'],
|
|
37
|
-
'vcs/commit-compare': ['sessionId', 'repositoryId', 'commit', 'parentIndex', 'id'],
|
|
37
|
+
'vcs/commit-compare': ['sessionId', 'repositoryId', 'commit', 'parentIndex', 'id', 'large'],
|
|
38
38
|
'vcs/file-history': ['sessionId', 'repositoryId', 'commit', 'parentIndex', 'id', 'offset', 'limit', 'follow'],
|
|
39
39
|
'vcs/blame': ['sessionId', 'repositoryId', 'commit', 'parentIndex', 'id', 'startLine', 'lineLimit'],
|
|
40
40
|
'vcs/revision-changes': ['sessionId', 'repositoryId', 'base', 'target'],
|
|
41
|
-
'vcs/revision-compare': ['sessionId', 'repositoryId', 'base', 'target', 'id'],
|
|
41
|
+
'vcs/revision-compare': ['sessionId', 'repositoryId', 'base', 'target', 'id', 'large'],
|
|
42
42
|
'vcs/revision-image': ['sessionId', 'repositoryId', 'base', 'target', 'id', 'side'],
|
|
43
43
|
'vcs/tree': ['sessionId', 'repositoryId', 'commit'],
|
|
44
44
|
'vcs/tree-file': ['sessionId', 'repositoryId', 'commit', 'path'],
|
|
@@ -68,6 +68,7 @@ function validatePayload(endpoint, payload) {
|
|
|
68
68
|
if(typeof payload.commit!=='string'||!/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/.test(payload.commit))throw invalid('Invalid commit id.');
|
|
69
69
|
if(payload.parentIndex!==undefined&&(!Number.isInteger(payload.parentIndex)||payload.parentIndex<0||payload.parentIndex>100))throw invalid('Invalid parent index.');
|
|
70
70
|
}
|
|
71
|
+
if(['vcs/commit-compare','vcs/revision-compare'].includes(endpoint)&&payload.large!==undefined&&typeof payload.large!=='boolean')throw invalid('Invalid large comparison mode.');
|
|
71
72
|
if(endpoint==='vcs/tree-segment'&&payload.offset!==undefined&&(!Number.isInteger(payload.offset)||payload.offset<0||payload.offset>16777216))throw invalid('Invalid segment offset.');
|
|
72
73
|
if(endpoint==='vcs/blame'&&((payload.startLine!==undefined&&(!Number.isInteger(payload.startLine)||payload.startLine<1||payload.startLine>100001))||(payload.lineLimit!==undefined&&(!Number.isInteger(payload.lineLimit)||payload.lineLimit<1||payload.lineLimit>500))))throw invalid('Invalid blame line window.');
|
|
73
74
|
if(endpoint==='vcs/file-history'&&payload.follow!==undefined&&typeof payload.follow!=='boolean')throw invalid('Invalid rename follow mode.');
|
|
@@ -111,9 +112,9 @@ async function readRepository(api, endpoint, repository, payload, {cwd, mode, si
|
|
|
111
112
|
case 'vcs/revision-changes':
|
|
112
113
|
return api.getRevisionChanges({...repository}, {base: payload.base, target: payload.target, signal});
|
|
113
114
|
case 'vcs/revision-compare':
|
|
114
|
-
return api.getRevisionComparison({...repository}, {base: payload.base, target: payload.target, id: payload.id, signal});
|
|
115
|
+
return api.getRevisionComparison({...repository}, {base: payload.base, target: payload.target, id: payload.id, ...(payload.large !== undefined ? {large: payload.large} : {}), signal});
|
|
115
116
|
case 'vcs/commit-compare':
|
|
116
|
-
return api.getCommitComparison({...repository}, {commit: payload.commit, parentIndex: payload.parentIndex ?? 0, id: payload.id, signal});
|
|
117
|
+
return api.getCommitComparison({...repository}, {commit: payload.commit, parentIndex: payload.parentIndex ?? 0, id: payload.id, ...(payload.large !== undefined ? {large: payload.large} : {}), signal});
|
|
117
118
|
case 'vcs/commit':
|
|
118
119
|
return api.getCommitDetails({...repository}, {commit: payload.commit, parentIndex: payload.parentIndex ?? 0, signal});
|
|
119
120
|
case 'vcs/history':
|
|
@@ -194,7 +195,7 @@ export function createHandler(ctx, api = adapter, maxActive = 4, { now = Date.no
|
|
|
194
195
|
// Both adapter operations revalidate the canonical root; comparison also validates
|
|
195
196
|
// the change ID against fresh status, so do not duplicate a full status scan here.
|
|
196
197
|
if(['vcs/history','vcs/commit','vcs/commit-compare','vcs/revision-changes','vcs/revision-compare','vcs/file-history','vcs/blame','vcs/tree','vcs/tree-file','vcs/tree-segment','vcs/commit-image','vcs/revision-image','vcs/workspace-image','vcs/references'].includes(endpoint)&&repository.type!=='git')throw invalid('History currently supports Git only.');
|
|
197
|
-
if(endpoint==='vcs/tree-segment'){if(activeSegments>=1)return failure('vcs/busy','A text segment is already loading. Please retry.');activeSegments++;segmentSlot=true;}
|
|
198
|
+
if(endpoint==='vcs/tree-segment'||(['vcs/commit-compare','vcs/revision-compare'].includes(endpoint)&&payload.large===true)){if(activeSegments>=1)return failure('vcs/busy','A text segment is already loading. Please retry.');activeSegments++;segmentSlot=true;}
|
|
198
199
|
if(['vcs/commit-image','vcs/revision-image','vcs/workspace-image'].includes(endpoint)){
|
|
199
200
|
if(activeImages>=2)return failure('vcs/busy','Too many image requests. Please retry.');
|
|
200
201
|
activeImages++;imageSlot=true;
|
package/package.json
CHANGED
package/text-content.mjs
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
/** Pure bounded byte decoding shared by working and committed previews. */
|
|
2
2
|
export const MAX_TEXT = 2 * 1024 * 1024;
|
|
3
|
+
export const MAX_LARGE_TEXT = 8 * 1024 * 1024;
|
|
4
|
+
|
|
5
|
+
/** Explicit historical mode only: complete UTF-8, never a truncated Diff input. */
|
|
6
|
+
export function decodeLargeText(buffer, signal) {
|
|
7
|
+
signal?.throwIfAborted();
|
|
8
|
+
if (buffer.length > MAX_LARGE_TEXT) throw new Error('Large comparison exceeds 8 MiB per side.');
|
|
9
|
+
if (buffer.some(byte => byte < 32 && ![9, 10, 12, 13].includes(byte))) {
|
|
10
|
+
throw new Error('Large comparison requires UTF-8 text without binary control bytes.');
|
|
11
|
+
}
|
|
12
|
+
let text;
|
|
13
|
+
try { text = new TextDecoder('utf-8', {fatal: true}).decode(buffer); }
|
|
14
|
+
catch { throw new Error('Large comparison requires valid UTF-8 text.'); }
|
|
15
|
+
let lines = 1, length = 0;
|
|
16
|
+
for (let index = 0; index < text.length; index++) {
|
|
17
|
+
if (index % 65536 === 0) signal?.throwIfAborted();
|
|
18
|
+
if (text[index] === '\n' || text[index] === '\r') {
|
|
19
|
+
if (text[index] === '\r' && text[index + 1] === '\n') index++;
|
|
20
|
+
if (++lines > 100000) throw new Error('Large comparison exceeds 100000 lines per side.');
|
|
21
|
+
length = 0;
|
|
22
|
+
} else if (++length > 65536) throw new Error('Large comparison exceeds 65536 characters per line.');
|
|
23
|
+
}
|
|
24
|
+
signal?.throwIfAborted();
|
|
25
|
+
return {text, encoding: 'UTF-8', totalBytes: buffer.length, lineCount: lines};
|
|
26
|
+
}
|
|
3
27
|
|
|
4
28
|
export function decode(buffer) {
|
|
5
29
|
if (buffer.length > MAX_TEXT) {
|
package/vcs.mjs
CHANGED
|
@@ -8,7 +8,7 @@ import path from 'node:path';
|
|
|
8
8
|
import { createHash } from 'node:crypto';
|
|
9
9
|
import { XMLParser, XMLValidator } from 'fast-xml-parser';
|
|
10
10
|
|
|
11
|
-
import {decode, MAX_TEXT} from './text-content.mjs';
|
|
11
|
+
import {decode, MAX_TEXT, MAX_LARGE_TEXT, decodeLargeText} from './text-content.mjs';
|
|
12
12
|
import {inside, confined} from './repository-path.mjs';
|
|
13
13
|
const xml = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '', parseTagValue: false, parseAttributeValue: false, trimValues: false, processEntities: true, isArray: name => ['entry', 'target', 'property'].includes(name) });
|
|
14
14
|
|
|
@@ -455,35 +455,48 @@ async function working(root, relative, svnLink = false) {
|
|
|
455
455
|
try { const info = await handle.stat(); if (!info.isFile()) throw new Error('Not a regular file'); const buffer = Buffer.alloc(MAX_TEXT + 1); const { bytesRead } = await handle.read(buffer, 0, buffer.length, 0); await confined(root, relative); return decode(buffer.subarray(0, bytesRead)); } finally { await handle.close(); }
|
|
456
456
|
}
|
|
457
457
|
async function gitBlob(root, revision, file) { return content(() => git(root, ['show', '--no-ext-diff', '--no-textconv', revision + ':' + file], MAX_TEXT)); }
|
|
458
|
-
async function historicalBlob(root,revision,file,signal){
|
|
458
|
+
async function historicalBlob(root,revision,file,signal,large=false){
|
|
459
459
|
const listing=(await git(root,['ls-tree','-z',revision,'--',file],MAX_TEXT,{signal})).toString('utf8');
|
|
460
460
|
const records=listing.split(String.fromCharCode(0)).filter(Boolean);
|
|
461
461
|
const record=records.find(row=>row.slice(row.indexOf(' ')+1)===file);
|
|
462
462
|
if(!record)throw new Error('Historical path missing');
|
|
463
463
|
const [mode,type,oid]=record.slice(0,record.indexOf(' ')).split(' ');
|
|
464
|
+
if(large&&(!['100644','100755'].includes(mode)||type!=='blob'))throw new Error('Large comparison requires a committed regular file.');
|
|
464
465
|
if(mode==='160000')return {text:oid,notice:'Submodule commit reference; repository contents are not loaded.'};
|
|
465
466
|
if(type!=='blob'||!['100644','100755','120000'].includes(mode))return {text:'',notice:'Unsupported historical object type.'};
|
|
467
|
+
if (large) {
|
|
468
|
+
const rawSize = (await git(root, ['cat-file', '-s', oid], MAX_TEXT, {signal})).toString('utf8').trim();
|
|
469
|
+
if (!/^\d+$/.test(rawSize)) throw new Error('Invalid historical object size.');
|
|
470
|
+
const size = Number(rawSize);
|
|
471
|
+
if (size > MAX_LARGE_TEXT) throw new Error('Large comparison exceeds 8 MiB per side.');
|
|
472
|
+
const buffer = await git(root, ['cat-file', 'blob', oid], MAX_LARGE_TEXT, {signal});
|
|
473
|
+
if (buffer.length !== size) throw new Error('Historical object size changed.');
|
|
474
|
+
return decodeLargeText(buffer, signal);
|
|
475
|
+
}
|
|
476
|
+
|
|
466
477
|
const result=await content(()=>git(root,['cat-file','blob',oid],MAX_TEXT,{signal}));
|
|
467
478
|
if(mode==='120000')result.notice=[result.notice,'Symbolic link target text; not followed.'].filter(Boolean).join(' ');
|
|
468
479
|
return result;
|
|
469
480
|
}
|
|
470
|
-
export async function getCommitComparison(repo,{commit,parentIndex=0,id,signal}={}){
|
|
481
|
+
export async function getCommitComparison(repo,{commit,parentIndex=0,id,signal,large=false}={}){
|
|
482
|
+
if(typeof large!=='boolean')throw new Error('Invalid large comparison mode');
|
|
471
483
|
if(typeof id!=='string'||!/^[a-f0-9]{64}$/.test(id))throw new Error('Invalid historical change id');
|
|
472
484
|
const details=await commitDetails(repo,{commit,parentIndex,signal});
|
|
473
485
|
const entry=details.changes.find(row=>row.id===id);
|
|
474
486
|
if(!entry)throw new Error('Historical change is not part of selected commit and parent');
|
|
475
|
-
const left=details.parent&&entry.status!=='added'?await historicalBlob(repo.root,details.parent,entry.oldPath||entry.path,signal):{text:''};
|
|
476
|
-
const right=entry.status!=='deleted'?await historicalBlob(repo.root,commit,entry.path,signal):{text:''};
|
|
477
|
-
return {...entry,left:{...left,label:details.parent||'Empty tree'},right:{...right,label:commit},binary:!!(left.binary||right.binary),notice:[left.notice,right.notice].filter(Boolean).join(' ')};
|
|
487
|
+
const left=details.parent&&entry.status!=='added'?await historicalBlob(repo.root,details.parent,entry.oldPath||entry.path,signal,large):{text:''};
|
|
488
|
+
const right=entry.status!=='deleted'?await historicalBlob(repo.root,commit,entry.path,signal,large):{text:''};
|
|
489
|
+
return {...entry,large,left:{...left,label:details.parent||'Empty tree'},right:{...right,label:commit},binary:!!(left.binary||right.binary),notice:[left.notice,right.notice].filter(Boolean).join(' ')};
|
|
478
490
|
}
|
|
479
|
-
export async function getRevisionComparison(repo,{base,target,id,signal}={}){
|
|
491
|
+
export async function getRevisionComparison(repo,{base,target,id,signal,large=false}={}){
|
|
492
|
+
if(typeof large!=='boolean')throw new Error('Invalid large comparison mode');
|
|
480
493
|
if(typeof id!=='string'||!/^[a-f0-9]{64}$/.test(id))throw new Error('Invalid revision change id');
|
|
481
494
|
const details=await getRevisionChanges(repo,{base,target,signal});
|
|
482
495
|
const entry=details.changes.find(row=>row.id===id);
|
|
483
496
|
if(!entry)throw new Error('Change is not part of selected revision pair');
|
|
484
|
-
const left=entry.status!=='added'?await historicalBlob(repo.root,base,entry.oldPath||entry.path,signal):{text:''};
|
|
485
|
-
const right=entry.status!=='deleted'?await historicalBlob(repo.root,target,entry.path,signal):{text:''};
|
|
486
|
-
return {...entry,base,target,left:{...left,label:base},right:{...right,label:target},binary:!!(left.binary||right.binary),notice:[left.notice,right.notice].filter(Boolean).join(' ')};
|
|
497
|
+
const left=entry.status!=='added'?await historicalBlob(repo.root,base,entry.oldPath||entry.path,signal,large):{text:''};
|
|
498
|
+
const right=entry.status!=='deleted'?await historicalBlob(repo.root,target,entry.path,signal,large):{text:''};
|
|
499
|
+
return {...entry,base,target,large,left:{...left,label:base},right:{...right,label:target},binary:!!(left.binary||right.binary),notice:[left.notice,right.notice].filter(Boolean).join(' ')};
|
|
487
500
|
}
|
|
488
501
|
async function properties(root, file, base) {
|
|
489
502
|
const result = parseXML(await svn(root, ['proplist', '--xml', '--verbose', ...(base ? ['--revision', 'BASE'] : []), '--', file + '@']));
|