@feng0531/dsh-code-review 0.60.1 → 0.61.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/CHANGELOG.md +5 -1
- package/CORE-ALIGNMENT-TASKS.md +2 -1
- package/README.md +1 -1
- package/RELEASE-NOTES.md +10 -7
- package/SVN-SUPPORT-MATRIX.md +2 -2
- package/package.json +1 -1
- package/src/svn.mjs +28 -16
package/CHANGELOG.md
CHANGED
|
@@ -2,9 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
本文件介绍对使用者有意义的变化。安装与参数见 [README](README.md),本次发布正文见 [发布说明](RELEASE-NOTES.md)。历史提交和安装包保留在 [GitHub Releases](https://github.com/f-e-n-g-0531/dsh-code-review/releases);本文不把开发日志当作功能介绍。
|
|
4
4
|
|
|
5
|
+
## 0.61.0 — SVN 目录复制后代覆盖
|
|
6
|
+
|
|
7
|
+
显式展开默认 status 隐藏的复制目录后代,冻结继承 copyfrom 来源;copied 的 normal/modified 后代按空目标旧侧新增审查,不把来源 BASE 误作目标旧侧。目录及后代分别覆盖,未全选、凭据或阻断子项使父目录覆盖不足。离线且共享原数量/字节预算,不新增来源正文读取或联网;复制替换仍阻断。
|
|
8
|
+
|
|
5
9
|
## 0.60.1 — SVN 阻断状态的准确诊断
|
|
6
10
|
|
|
7
|
-
对替换/复制替换、缺失、阻塞、不完整、文本/属性/树冲突、switched 与 external 分别给出稳定 blockCode 和解释,预览及结构化报告保留原因码。普通替换实测 local BASE 无 pristine,复制替换可能返回来源 BASE,因此继续明确阻断,不合成旧侧、不冒充新增。诊断改进不表示真实双侧替换审查已支持。
|
|
11
|
+
对替换/复制替换、缺失、阻塞、不完整、文本/属性/树冲突、switched 与 external 分别给出稳定 blockCode 和解释,预览及结构化报告保留原因码。普通替换实测 local BASE 无 pristine,复制替换可能返回来源 BASE,因此继续明确阻断,不合成旧侧、不冒充新增。诊断改进不表示真实双侧替换审查已支持。463 项串行回归、宿主、隔离安装和四矩阵 CI 通过;原产物/npm/Release 119,462 字节逐字节一致,双哈希、清单与冻结发布说明通过。
|
|
8
12
|
|
|
9
13
|
## 0.60.0 — SVN 普通目录新增与删除后代覆盖
|
|
10
14
|
|
package/CORE-ALIGNMENT-TASKS.md
CHANGED
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
|
|
33
33
|
## 当前验证结论
|
|
34
34
|
|
|
35
|
-
- 最近完整回归:
|
|
35
|
+
- 最近完整回归:463 项通过。
|
|
36
|
+
- v0.60.1 已交付 SVN 阻断状态稳定诊断及预览/报告原因码;普通与复制替换仍明确拒绝,不冒充真实双侧支持。463 项串行回归、宿主、隔离安装和四矩阵 CI 通过;原产物/npm/Release 119,462 字节逐字节一致,双哈希、清单与冻结发布说明通过。全面 SVN 目标继续。
|
|
36
37
|
- v0.60.0 已交付 SVN 普通目录新增/删除的后代逐项覆盖,离线补齐默认 status 隐藏的删除后代;显式选择、凭据排除与原预算仍生效。461 项串行回归、宿主、隔离安装和四矩阵 CI 通过;原产物/npm/Release 118,284 字节逐字节一致,双哈希、清单与冻结发布说明通过。目录复制、替换及历史等仍在后续矩阵中。
|
|
37
38
|
- v0.59.0 已交付 SVN 同仓库复制新增文件和文件移动的新增/删除独立覆盖。459 项串行回归、实际宿主、隔离安装和四组 CI 通过;原产物/npm/Release 117,098 字节逐字节一致,SHA-256、SHA-512、清单与冻结发布说明通过。复制替换、目录后代和历史审查等仍未闭环。
|
|
38
39
|
- v0.58.1 已交付 SVN 根目录 mergeinfo 属性预览修复及全面支持矩阵。456 项串行回归、实际宿主、隔离安装与四组 CI 通过;npm、Release 与原包 116,254 字节逐字节一致,SHA-256、SHA-512、清单与冻结发布说明核验通过。SVN 全面扩展目标仍在进行,历史与特殊状态尚未全部支持。
|
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ commit 与 baseRevision/targetRevision 不能同时使用。oldContextPaths 只
|
|
|
89
89
|
- 不执行项目代码、构建脚本或模型生成的 shell 命令。模型只能在批准的快照中查找、搜索和读取。
|
|
90
90
|
- 常见凭据路径会被排除,但普通源码也可能含秘密。路径过滤不能代替人工检查。
|
|
91
91
|
- 二进制、不能解码的文件、链接和超限内容会被阻断,不会自动截断后假装完成。
|
|
92
|
-
- SVN 同仓库复制新增文件以空目标旧侧审查,copyfrom URL/revision/UUID
|
|
92
|
+
- SVN 同仓库复制新增文件以空目标旧侧审查,copyfrom URL/revision/UUID 在预览中披露,仅作来源信息,不自动读取来源正文,也不认定为已验证重命名。文件移动的新增/删除两侧分别覆盖。普通目录新增/删除会逐项列出后代;选择目录不会自动批准后代,未全选或子项阻断会标明覆盖不足。同仓库目录复制会显式展开后代,将继承 copied 的正常/修改节点作为空目标旧侧的新增审查,来源身份逐项披露。替换、切换目录、外部工作副本、冲突及部分属性状态仍未支持;含 svn:keywords 的文件仍阻断。全面扩展进度见 [SVN 支持矩阵](SVN-SUPPORT-MATRIX.md)。
|
|
93
93
|
|
|
94
94
|
## 默认规模限制
|
|
95
95
|
|
package/RELEASE-NOTES.md
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
|
-
# v0.
|
|
1
|
+
# v0.61.0 发布说明
|
|
2
2
|
|
|
3
|
-
## SVN
|
|
3
|
+
## SVN 目录复制后代覆盖
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
支持同仓库目录复制的显式后代采集。默认 SVN status 可能只列复制根,本版在原数量与共享元数据预算内递归列出目标后代,并在采集末尾复核。
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
7
|
+
- copied 的 normal/modified 后代属于目标新增:旧侧为空,不能用来源 BASE 替代目标旧侧。
|
|
8
|
+
- 继承 copyfrom URL/revision/UUID 按目标相对路径明确记录,预览披露并核验节点身份。
|
|
9
|
+
- 目录与后代分别覆盖;未全选、凭据或阻断子项不被目录成功状态掩盖。
|
|
10
|
+
- 离线测试覆盖复制后修改、稳定快照、预览零发送、显式选择与超限。无新增联网或来源正文读取。
|
|
10
11
|
|
|
11
|
-
|
|
12
|
+
## 当前限制
|
|
13
|
+
|
|
14
|
+
复制替换、关键字映射、固定 revision 历史等仍在 [SVN 支持矩阵](SVN-SUPPORT-MATRIX.md) 中,全面目标尚未完成。本版不推定语义重命名或来源正文已被审查。
|
|
12
15
|
|
|
13
16
|
日常使用见 [README](README.md)。
|
package/SVN-SUPPORT-MATRIX.md
CHANGED
|
@@ -17,10 +17,10 @@
|
|
|
17
17
|
| 常规文件修改、普通新增、删除 | 已有 BASE/工作字节与属性捕获测试 | 维持并扩充组合验收 |
|
|
18
18
|
| 子目录属性变更 | 已有独立属性审查测试 | 第一批 |
|
|
19
19
|
| 根目录属性/mergeinfo | v0.58.1 已发布;独立复现后修复,指定三文件和默认预览均通过、0 模型发送、状态不变;456 项串行回归、宿主、隔离安装和四矩阵 CI 通过,原产物/npm/Release 116,254 字节逐字节一致及双哈希/清单/发布说明通过 | 第一批先发布此修复 |
|
|
20
|
-
| 复制/移动、copyfrom 及目录结构变更 | v0.59.0 已发布同仓库复制新增文件及文件移动的新增/删除独立覆盖:新增目标旧侧为空,copyfrom 只作来源信息,预览披露并冻结身份;离线与凭据来源拒绝测试通过。459 项串行回归、宿主、隔离安装、四矩阵 CI 通过;原产物/npm/Release 117,098
|
|
20
|
+
| 复制/移动、copyfrom 及目录结构变更 | v0.59.0 已发布同仓库复制新增文件及文件移动的新增/删除独立覆盖:新增目标旧侧为空,copyfrom 只作来源信息,预览披露并冻结身份;离线与凭据来源拒绝测试通过。459 项串行回归、宿主、隔离安装、四矩阵 CI 通过;原产物/npm/Release 117,098 字节逐字节一致,双哈希/清单/冻结发布说明通过。本地新增同仓库目录复制后代展开与继承来源身份;目标旧侧为空,预览披露;离线、改后代、凭据排除、显式选择和超限已测试,待 v0.61.0 发布核验。复制替换仍未支持 | 第二批:明确旧侧来源和后代覆盖 |
|
|
21
21
|
| 普通新增/删除目录及后代 | v0.60.0 已发布:补齐默认 status 隐藏的删除后代,离线递归枚举与末尾清单复核;普通结构目录只在后代全选且可审查时开放,凭据/阻断子项导致目录覆盖不足。461 项串行回归、宿主、隔离安装与四矩阵 CI 通过;原产物/npm/Release 118,284 字节逐字节一致,双哈希/清单/冻结发布说明通过 | 第二批普通目录子批 |
|
|
22
22
|
| 替换、删除再新增、文件/目录类型变化 | 普通与复制替换分别用 SVN_REPLACEMENT/SVN_COPIED_REPLACEMENT 诊断;离线探测确认前者 BASE 无 pristine,后者可能返回复制来源,禁止伪造旧侧。真实双侧替换仍未支持 | 第二批:区分 BASE 旧节点与目标节点 |
|
|
23
|
-
| 缺失、文本/属性/树冲突、阻塞 |
|
|
23
|
+
| 缺失、文本/属性/树冲突、阻塞 | v0.60.1 已发布稳定状态诊断:SVN_MISSING_NODE/OBSTRUCTED_NODE/INCOMPLETE_NODE/TEXT_CONFLICT/PROPERTY_CONFLICT/TREE_CONFLICT 等,预览与报告透传 blockCode。463 项串行回归、宿主、隔离安装、四矩阵 CI 与原包/npm/Release 119,462 字节、双哈希/清单/发布说明通过。诊断不算真实双侧支持,仍需后续实现 | 第三批:明确状态诊断;不能合成不存在的工作侧或声称审核通过 |
|
|
24
24
|
| svn:keywords、svn:eol-style | keywords 当前阻断;换行保留原文但无规范化映射 | 第三批:双侧可验证映射,保留原字节和定位 |
|
|
25
25
|
| svn:special/链接 | 当前阻断,不跟随链接取正文 | 第三批:如支持只审链接目标数据;不沿链接读取文件 |
|
|
26
26
|
| switched、externals、稀疏/混合 revision | 多数拒绝;调用方/依赖链要求祖先 normal | 第四批:明确仓库边界与独立来源,禁止隐式跨仓读取 |
|
package/package.json
CHANGED
package/src/svn.mjs
CHANGED
|
@@ -23,11 +23,11 @@ async function properties(root, name, baseline, options) {
|
|
|
23
23
|
}
|
|
24
24
|
return Object.fromEntries(Object.entries(result).sort(([a], [b]) => a.localeCompare(b)));
|
|
25
25
|
}
|
|
26
|
-
// Default status hides deleted directory descendants. Expand only
|
|
27
|
-
//
|
|
28
|
-
async function
|
|
26
|
+
// Default status hides deleted and copied directory descendants. Expand only
|
|
27
|
+
// explicit structural roots under the shared metadata and change-count caps.
|
|
28
|
+
async function structuralEntries(root, entries, options, limit) {
|
|
29
29
|
const byPath = new Map(entries.map(entry => [entry['@_path'].replaceAll('\\', '/'), entry]));
|
|
30
|
-
const roots = entries.filter(entry => entry['wc-status']?.['@_item'] === 'deleted').map(entry => entry['@_path'].replaceAll('\\', '/')).sort();
|
|
30
|
+
const roots = entries.filter(entry => entry['wc-status']?.['@_item'] === 'deleted' || (entry['wc-status']?.['@_item'] === 'added' && entry['wc-status']?.['@_copied'] === 'true')).map(entry => entry['@_path'].replaceAll('\\', '/')).sort();
|
|
31
31
|
const visited = [];
|
|
32
32
|
let bytes = 0;
|
|
33
33
|
for (const name of roots) {
|
|
@@ -41,8 +41,11 @@ async function deletedEntries(root, entries, options, limit) {
|
|
|
41
41
|
const child = entry['@_path'].replaceAll('\\', '/');
|
|
42
42
|
relativePath(child);
|
|
43
43
|
if (child !== name && !child.startsWith(name + '/')) throw new Error('SVN deleted descendant escaped scope');
|
|
44
|
-
|
|
45
|
-
if (
|
|
44
|
+
const parentState = byPath.get(name)['wc-status'];
|
|
45
|
+
if (parentState['@_item'] === 'deleted' && entry['wc-status']?.['@_item'] !== 'deleted') throw new Error('Unexpected SVN deleted descendant state');
|
|
46
|
+
const item = structuredClone(entry);
|
|
47
|
+
if (parentState['@_item'] === 'added' && child !== name && item['wc-status']?.['@_copied'] === 'true') item.copyRoot = name;
|
|
48
|
+
if (!byPath.has(child) || item.copyRoot) byPath.set(child, item);
|
|
46
49
|
if (byPath.size > limit) throw new Error('Change count limit exceeded');
|
|
47
50
|
}
|
|
48
51
|
}
|
|
@@ -57,7 +60,7 @@ export async function captureSvn(cwd, options = {}) {
|
|
|
57
60
|
const before = await svn(root, statusArgs, options);
|
|
58
61
|
const originalEntries = xml(before).status.target.flatMap(t => t.entry ?? []);
|
|
59
62
|
if (originalEntries.length > maxFiles) throw new Error('Change count limit exceeded');
|
|
60
|
-
const entries = await
|
|
63
|
+
const entries = await structuralEntries(root, originalEntries, options, maxFiles);
|
|
61
64
|
const known = new Set(entries.map(e => e['@_path'].replaceAll('\\', '/')));
|
|
62
65
|
if (selectedPaths?.some(p => !known.has(p))) throw new Error('Selected path is not a current change');
|
|
63
66
|
const files = [];
|
|
@@ -73,24 +76,33 @@ export async function captureSvn(cwd, options = {}) {
|
|
|
73
76
|
}
|
|
74
77
|
try {
|
|
75
78
|
if (name !== '.') relativePath(name);
|
|
76
|
-
const
|
|
79
|
+
const copiedDescendant = !!entry.copyRoot && ['normal', 'modified', 'added'].includes(status) && state['@_copied'] === 'true';
|
|
80
|
+
const issue = svnStateIssue(copiedDescendant ? {...state, '@_item':'added'} : state);
|
|
77
81
|
if (issue) { item.blockCode = issue.code; throw new Error(issue.reason); }
|
|
78
|
-
const hasBase = !['added', 'unversioned'].includes(status);
|
|
82
|
+
const hasBase = !copiedDescendant && !['added', 'unversioned'].includes(status);
|
|
79
83
|
let kind;
|
|
80
84
|
if (status === 'unversioned') kind = (await lstat(path.join(root, name))).isDirectory() ? 'dir' : 'file';
|
|
81
85
|
else {
|
|
82
86
|
const node = xml(await svn(root, ['info', '--xml', '--', target(name)], options)).info.entry[0];
|
|
83
87
|
kind = node['@_kind'];
|
|
84
88
|
if (state['@_copied'] === 'true' || node['wc-info']?.['copy-from-url']) {
|
|
85
|
-
|
|
86
|
-
if (
|
|
89
|
+
let source = node['wc-info']?.['copy-from-url'], revision = node['wc-info']?.['copy-from-rev'];
|
|
90
|
+
if (!source && copiedDescendant) {
|
|
91
|
+
const parent = files.find(file => file.path === entry.copyRoot);
|
|
92
|
+
if (!parent?.copySource || parent.eligibility !== 'reviewable') throw new Error('Copied ancestor identity unavailable');
|
|
93
|
+
source = parent.copySource.url.replace(/\/$/,'') + '/' + name.slice(entry.copyRoot.length+1).split('/').map(encodeURIComponent).join('/');
|
|
94
|
+
revision = parent.copySource.revision;
|
|
95
|
+
item.copyAncestor = entry.copyRoot;
|
|
96
|
+
}
|
|
97
|
+
if ((!copiedDescendant && status !== 'added') || !['file','dir'].includes(kind) || typeof source !== 'string' || !source || !/^[0-9]+$/.test(String(revision)) || node.repository?.uuid !== info.repository.uuid || node.repository?.root !== info.repository.root) throw new Error('Copied node requires a same-repository added target with fixed copy source');
|
|
87
98
|
if (isSecretPath(decodeURIComponent(new URL(source).pathname))) throw new Error('Credential copy source excluded');
|
|
88
99
|
item.copySource = { url: source, revision: String(revision), repositoryUuid: node.repository.uuid };
|
|
89
100
|
item.copyNotice = 'Target addition is reviewed against an empty target baseline; copy source is provenance only, not an approved source body or verified rename.';
|
|
90
|
-
item.copyNodeIdentity = hash(JSON.stringify([node.url, node['@_kind'], node['@_revision'], node['wc-info']?.schedule, node['wc-info']?.['wcroot-abspath'],
|
|
101
|
+
item.copyNodeIdentity = hash(JSON.stringify([node.url, node['@_kind'], node['@_revision'], node['wc-info']?.schedule, node['wc-info']?.['wcroot-abspath'], node['wc-info']?.['copy-from-url'] ?? null, node['wc-info']?.['copy-from-rev'] ?? null, node.repository]));
|
|
91
102
|
}
|
|
92
103
|
}
|
|
93
104
|
item.kind = kind;
|
|
105
|
+
if (copiedDescendant) item.targetAddition = true;
|
|
94
106
|
item.rightExists = status !== 'deleted';
|
|
95
107
|
const leftProps = hasBase ? await properties(root, name, true, options) : {};
|
|
96
108
|
const rightProps = status === 'deleted' || status === 'unversioned' ? {} : await properties(root, name, false, options);
|
|
@@ -103,7 +115,7 @@ export async function captureSvn(cwd, options = {}) {
|
|
|
103
115
|
item.propertyIdentity = hash(JSON.stringify([leftProps, rightProps]));
|
|
104
116
|
if (kind === 'dir') {
|
|
105
117
|
if (status === 'unversioned') throw new Error('Unversioned directory requires explicit descendant tracking');
|
|
106
|
-
if (status === 'deleted' || status === 'added') {
|
|
118
|
+
if (status === 'deleted' || status === 'added' || copiedDescendant) {
|
|
107
119
|
const descendants = entries.filter(entry => entry['@_path'].replaceAll('\\', '/').startsWith(name + '/')).map(entry => entry['@_path'].replaceAll('\\', '/'));
|
|
108
120
|
if (selectedPaths && descendants.some(child => !selectedPaths.includes(child))) throw new Error('Directory descendants are not all explicitly selected');
|
|
109
121
|
item.structuralDescendants = descendants;
|
|
@@ -121,7 +133,7 @@ export async function captureSvn(cwd, options = {}) {
|
|
|
121
133
|
}
|
|
122
134
|
const after = await svn(root, statusArgs, options);
|
|
123
135
|
if (!isDeepStrictEqual(xml(after), xml(before))) throw new Error('Working copy changed during capture');
|
|
124
|
-
if (!isDeepStrictEqual(await
|
|
136
|
+
if (!isDeepStrictEqual(await structuralEntries(root, xml(after).status.target.flatMap(t => t.entry ?? []), options, maxFiles), entries)) throw new Error('Deleted descendants changed during capture');
|
|
125
137
|
// A directory must not look complete when any descendant was blocked/excluded.
|
|
126
138
|
for (const item of files.filter(f => f.structuralDescendants).sort((a,b) => b.path.length - a.path.length)) {
|
|
127
139
|
if (item.structuralDescendants.some(name => files.find(f => f.path === name)?.eligibility !== 'reviewable')) {
|
|
@@ -132,11 +144,11 @@ export async function captureSvn(cwd, options = {}) {
|
|
|
132
144
|
for (const item of files.filter(f => f.eligibility === 'reviewable')) {
|
|
133
145
|
if (item.copySource) {
|
|
134
146
|
const node = xml(await svn(root, ['info', '--xml', '--', target(item.path)], options)).info.entry[0];
|
|
135
|
-
const identity = hash(JSON.stringify([node.url, node['@_kind'], node['@_revision'], node['wc-info']?.schedule, node['wc-info']?.['wcroot-abspath'], node['wc-info']?.['copy-from-url'],
|
|
147
|
+
const identity = hash(JSON.stringify([node.url, node['@_kind'], node['@_revision'], node['wc-info']?.schedule, node['wc-info']?.['wcroot-abspath'], node['wc-info']?.['copy-from-url'] ?? null, node['wc-info']?.['copy-from-rev'] ?? null, node.repository]));
|
|
136
148
|
if (identity !== item.copyNodeIdentity) throw new Error('Copy source identity changed during capture');
|
|
137
149
|
}
|
|
138
150
|
if (item.kind === 'file' && item.rawStatus !== 'deleted' && (await readLocal(root, item.path, options)).hash !== item.right.hash) throw new Error('Content changed during capture');
|
|
139
|
-
const left = !['added', 'unversioned'].includes(item.rawStatus) ? await properties(root, item.path, true, options) : {};
|
|
151
|
+
const left = !item.targetAddition && !['added', 'unversioned'].includes(item.rawStatus) ? await properties(root, item.path, true, options) : {};
|
|
140
152
|
const right = ['deleted', 'unversioned'].includes(item.rawStatus) ? {} : await properties(root, item.path, false, options);
|
|
141
153
|
if (hash(JSON.stringify([left, right])) !== item.propertyIdentity) throw new Error('Properties changed during capture');
|
|
142
154
|
}
|