nano-git 0.2.3 → 0.3.1
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 +6 -0
- package/dist/workdir/change-index-plan.mjs +76 -0
- package/dist/workdir/change-index.d.mts +21 -0
- package/dist/workdir/change-index.mjs +413 -0
- package/dist/workdir/core.d.mts +54 -22
- package/dist/workdir/directory-view.mjs +197 -0
- package/dist/workdir/dirty-dir-plan.mjs +73 -0
- package/dist/workdir/dirty-dir.d.mts +28 -0
- package/dist/workdir/dirty-dir.mjs +32 -0
- package/dist/workdir/file-backend.mjs +96 -10
- package/dist/workdir/memory-backend.mjs +31 -12
- package/dist/workdir/nodes.mjs +1 -7
- package/dist/workdir/session-internal.mjs +216 -9
- package/dist/workdir/session-transaction.mjs +115 -0
- package/dist/workdir/session.mjs +103 -230
- package/dist/workdir/sqlite-backend.mjs +149 -48
- package/dist/workdir/state-store.d.mts +19 -8
- package/dist/workdir/write-tree.mjs +104 -40
- package/package.json +160 -199
- package/dist/workdir/change-log.d.mts +0 -25
- package/dist/workdir/change-log.mjs +0 -69
package/README.md
CHANGED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { resolvePath } from "./session-internal.mjs";
|
|
2
|
+
//#region src/workdir/change-index-plan.ts
|
|
3
|
+
/**
|
|
4
|
+
* Virtual Workdir change-index 刷新策略
|
|
5
|
+
*
|
|
6
|
+
* 把 session 写路径里的“是否允许增量刷新”判断与
|
|
7
|
+
* “应该执行哪种 change-index 更新动作”集中到单独模块。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 创建 change-index 刷新策略器。
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const planner = createChangeIndexPlanner(source, state, {
|
|
15
|
+
* rebuildAll() {},
|
|
16
|
+
* refreshPath() {},
|
|
17
|
+
* rewriteRename() {},
|
|
18
|
+
* writeCopy() {},
|
|
19
|
+
* });
|
|
20
|
+
* expect(planner.planRefreshForPath("a.txt").kind).toBe("refresh-path");
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
function createChangeIndexPlanner(source, state, actions) {
|
|
24
|
+
const canIncrementallyRefreshPath = (path, options) => {
|
|
25
|
+
const record = state.getChangeRecord(path);
|
|
26
|
+
if (record !== null && record.source !== null) return false;
|
|
27
|
+
const resolved = resolvePath(source, state, path);
|
|
28
|
+
if (!resolved.found || resolved.node === null) return options?.treatMissingAsIncremental === true;
|
|
29
|
+
return resolved.node.state.kind !== "directory";
|
|
30
|
+
};
|
|
31
|
+
const canIncrementallyWriteCopy = (from) => {
|
|
32
|
+
const resolved = resolvePath(source, state, from);
|
|
33
|
+
if (!resolved.found || resolved.node === null) return false;
|
|
34
|
+
return resolved.node.state.kind !== "directory";
|
|
35
|
+
};
|
|
36
|
+
return {
|
|
37
|
+
apply(plan) {
|
|
38
|
+
switch (plan.kind) {
|
|
39
|
+
case "rebuild-all":
|
|
40
|
+
actions.rebuildAll();
|
|
41
|
+
return;
|
|
42
|
+
case "refresh-path":
|
|
43
|
+
actions.refreshPath(plan.path);
|
|
44
|
+
return;
|
|
45
|
+
case "rewrite-rename":
|
|
46
|
+
actions.rewriteRename(plan.from, plan.to);
|
|
47
|
+
return;
|
|
48
|
+
case "write-copy":
|
|
49
|
+
actions.writeCopy(plan.from, plan.to);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
planRefreshForPath(path, options) {
|
|
54
|
+
return canIncrementallyRefreshPath(path, options) ? {
|
|
55
|
+
kind: "refresh-path",
|
|
56
|
+
path
|
|
57
|
+
} : { kind: "rebuild-all" };
|
|
58
|
+
},
|
|
59
|
+
planRewriteForRename(from, to) {
|
|
60
|
+
return canIncrementallyRefreshPath(from) && canIncrementallyRefreshPath(to, { treatMissingAsIncremental: true }) ? {
|
|
61
|
+
kind: "rewrite-rename",
|
|
62
|
+
from,
|
|
63
|
+
to
|
|
64
|
+
} : { kind: "rebuild-all" };
|
|
65
|
+
},
|
|
66
|
+
planWriteForCopy(from, to) {
|
|
67
|
+
return canIncrementallyWriteCopy(from) && canIncrementallyRefreshPath(to, { treatMissingAsIncremental: true }) ? {
|
|
68
|
+
kind: "write-copy",
|
|
69
|
+
from,
|
|
70
|
+
to
|
|
71
|
+
} : { kind: "rebuild-all" };
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
//#endregion
|
|
76
|
+
export { createChangeIndexPlanner };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { VirtualDiffObject, VirtualDiffSource } from "./core.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/workdir/change-index.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* 规范化变更记录
|
|
6
|
+
*
|
|
7
|
+
* 仅保留相对 baseTree 的最终净效应;
|
|
8
|
+
* 若路径已恢复为 clean,则不保存记录。
|
|
9
|
+
*/
|
|
10
|
+
interface NormalizedChangeRecord {
|
|
11
|
+
/** 当前路径 */
|
|
12
|
+
readonly path: string;
|
|
13
|
+
/** 变更前对象 */
|
|
14
|
+
readonly previous: VirtualDiffObject | null;
|
|
15
|
+
/** 变更后对象 */
|
|
16
|
+
readonly current: VirtualDiffObject | null;
|
|
17
|
+
/** rename/copy 来源 */
|
|
18
|
+
readonly source: VirtualDiffSource | null;
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { NormalizedChangeRecord };
|
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
import { hashObject } from "../core/hash-digest.mjs";
|
|
2
|
+
import { readRepoBlobContent, readRepoTree } from "./origin.mjs";
|
|
3
|
+
import { getDirectoryChildrenView, joinChildPath, resolveCurrentLeafAtPath } from "./session-internal.mjs";
|
|
4
|
+
import { observeListedDirectoryChild } from "./directory-view.mjs";
|
|
5
|
+
//#region src/workdir/change-index.ts
|
|
6
|
+
/**
|
|
7
|
+
* Virtual Workdir 规范化变更索引
|
|
8
|
+
*
|
|
9
|
+
* 第一阶段先完成模型切换:
|
|
10
|
+
* - 对外 `diff()` 改为读取规范化变更索引
|
|
11
|
+
* - 写事务结束前重建净效应表并持久化
|
|
12
|
+
*
|
|
13
|
+
* 当前实现仍复用全量快照算法重建索引,
|
|
14
|
+
* 后续阶段再替换为真正的增量维护。
|
|
15
|
+
*/
|
|
16
|
+
const baseSnapshotCache = /* @__PURE__ */ new WeakMap();
|
|
17
|
+
/**
|
|
18
|
+
* 重建当前 session 的规范化变更索引。
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* const records = rebuildNormalizedChangeIndex(repo.objects, state);
|
|
23
|
+
* expect(records.map((record) => record.path)).toEqual(["hello.txt"]);
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
function rebuildNormalizedChangeIndex(source, state, cache) {
|
|
27
|
+
const baseSnapshot = baseSnapshotViewForTree(source, state.readBaseTree());
|
|
28
|
+
const baseEntries = baseSnapshot.entries;
|
|
29
|
+
const currentEntries = snapshotCurrentTree(source, state, cache);
|
|
30
|
+
const baseByPath = baseSnapshot.byPath;
|
|
31
|
+
const currentByPath = new Map(currentEntries.map((entry) => [entry.path, entry]));
|
|
32
|
+
const deletes = /* @__PURE__ */ new Map();
|
|
33
|
+
const adds = /* @__PURE__ */ new Map();
|
|
34
|
+
const out = [];
|
|
35
|
+
const allPaths = /* @__PURE__ */ new Set([...baseByPath.keys(), ...currentByPath.keys()]);
|
|
36
|
+
for (const path of Array.from(allPaths).sort()) {
|
|
37
|
+
const previous = baseByPath.get(path) ?? null;
|
|
38
|
+
const current = currentByPath.get(path) ?? null;
|
|
39
|
+
if (previous !== null && current !== null) {
|
|
40
|
+
if (!isSameObject(previous.object, current.object)) out.push({
|
|
41
|
+
path,
|
|
42
|
+
previous: previous.object,
|
|
43
|
+
current: current.object,
|
|
44
|
+
source: null
|
|
45
|
+
});
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (previous !== null) {
|
|
49
|
+
deletes.set(path, {
|
|
50
|
+
path,
|
|
51
|
+
previous: previous.object,
|
|
52
|
+
current: null,
|
|
53
|
+
source: null
|
|
54
|
+
});
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (current !== null) adds.set(path, {
|
|
58
|
+
path,
|
|
59
|
+
previous: null,
|
|
60
|
+
current: current.object,
|
|
61
|
+
source: null
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const unmatchedDeletesBySignature = indexDeletesBySignature(deletes);
|
|
65
|
+
const matchedDeletePaths = /* @__PURE__ */ new Set();
|
|
66
|
+
for (const [path, addRecord] of Array.from(adds.entries()).sort(([left], [right]) => left.localeCompare(right))) {
|
|
67
|
+
const current = currentByPath.get(path);
|
|
68
|
+
if (current === void 0 || current.originSignature === null) continue;
|
|
69
|
+
const renameFrom = (unmatchedDeletesBySignature.get(current.originSignature) ?? []).find((candidate) => !matchedDeletePaths.has(candidate.path));
|
|
70
|
+
if (renameFrom !== void 0) {
|
|
71
|
+
matchedDeletePaths.add(renameFrom.path);
|
|
72
|
+
adds.delete(path);
|
|
73
|
+
deletes.delete(renameFrom.path);
|
|
74
|
+
out.push({
|
|
75
|
+
path,
|
|
76
|
+
previous: renameFrom.previous,
|
|
77
|
+
current: addRecord.current,
|
|
78
|
+
source: {
|
|
79
|
+
kind: "rename",
|
|
80
|
+
path: renameFrom.path
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
const copyFrom = findCopySource(baseEntries, current);
|
|
86
|
+
if (copyFrom !== null) {
|
|
87
|
+
adds.delete(path);
|
|
88
|
+
out.push({
|
|
89
|
+
path,
|
|
90
|
+
previous: null,
|
|
91
|
+
current: addRecord.current,
|
|
92
|
+
source: {
|
|
93
|
+
kind: "copy",
|
|
94
|
+
path: copyFrom.path
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
out.push(...deletes.values(), ...adds.values());
|
|
100
|
+
return out.sort((left, right) => left.path.localeCompare(right.path));
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* 将规范化变更索引导出为公开 diff 结果。
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* ```ts
|
|
107
|
+
* const diff = exportVirtualDiffFromChangeRecords(records);
|
|
108
|
+
* expect(diff).toHaveLength(1);
|
|
109
|
+
* ```
|
|
110
|
+
*/
|
|
111
|
+
function exportVirtualDiffFromChangeRecords(records) {
|
|
112
|
+
return records.map((record) => {
|
|
113
|
+
if (record.previous === null && record.current !== null) return {
|
|
114
|
+
kind: "create",
|
|
115
|
+
path: record.path,
|
|
116
|
+
current: record.current,
|
|
117
|
+
source: record.source ?? void 0
|
|
118
|
+
};
|
|
119
|
+
if (record.previous !== null && record.current === null) return {
|
|
120
|
+
kind: "remove",
|
|
121
|
+
path: record.path,
|
|
122
|
+
previous: record.previous
|
|
123
|
+
};
|
|
124
|
+
if (record.previous !== null && record.current !== null) return {
|
|
125
|
+
kind: "update",
|
|
126
|
+
path: record.path,
|
|
127
|
+
previous: record.previous,
|
|
128
|
+
current: record.current,
|
|
129
|
+
changes: diffChanges(record.previous, record.current),
|
|
130
|
+
source: record.source ?? void 0
|
|
131
|
+
};
|
|
132
|
+
throw new Error(`Invalid normalized change record at path: ${record.path}`);
|
|
133
|
+
}).sort((left, right) => left.path.localeCompare(right.path));
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* 将新索引完整写回状态存储。
|
|
137
|
+
*/
|
|
138
|
+
function replaceChangeRecords(state, records) {
|
|
139
|
+
const nextByPath = new Map(records.map((record) => [record.path, record]));
|
|
140
|
+
for (const existing of state.listChangeRecords()) if (!nextByPath.has(existing.path)) state.deleteChangeRecord(existing.path);
|
|
141
|
+
for (const record of records) state.setChangeRecord(record);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* 仅刷新单一路径的规范化变更记录。
|
|
145
|
+
*
|
|
146
|
+
* 适用于同路径叶子节点写入等高频场景,
|
|
147
|
+
* 可避免在简单写操作后重建整张索引。
|
|
148
|
+
*/
|
|
149
|
+
function refreshChangeRecordForPath(source, state, path, cache) {
|
|
150
|
+
const nextRecord = computeChangeRecordForPath(source, state, path, state.getChangeRecord(path), cache);
|
|
151
|
+
if (nextRecord === null) {
|
|
152
|
+
state.deleteChangeRecord(path);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
state.setChangeRecord(nextRecord);
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* 将单一路径的变更记录折叠为 rename 目标路径。
|
|
159
|
+
*
|
|
160
|
+
* 仅适用于叶子节点 rename;
|
|
161
|
+
* 目录及无法判定来源的情况应由调用方回退到全量重建。
|
|
162
|
+
*/
|
|
163
|
+
function rewriteChangeRecordForRename(source, state, from, to, cache) {
|
|
164
|
+
const previousRecord = state.getChangeRecord(from);
|
|
165
|
+
const currentTarget = snapshotCurrentEntryAtPath(source, state, to, cache);
|
|
166
|
+
if (currentTarget === null) throw new Error(`Cannot rewrite rename change record for missing path: ${to}`);
|
|
167
|
+
const nextRecord = computeRenameRecordForPath(source, state, from, to, previousRecord, currentTarget);
|
|
168
|
+
if (nextRecord === null) throw new Error(`Cannot rewrite rename change record from '${from}' to '${to}'`);
|
|
169
|
+
state.deleteChangeRecord(from);
|
|
170
|
+
state.setChangeRecord(nextRecord);
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 为 copy 目标路径写入折叠后的变更记录。
|
|
174
|
+
*
|
|
175
|
+
* 仅适用于叶子节点 copy;
|
|
176
|
+
* session-only 来源允许退化为普通 create。
|
|
177
|
+
*/
|
|
178
|
+
function writeChangeRecordForCopy(source, state, from, to, cache) {
|
|
179
|
+
const sourceRecord = state.getChangeRecord(from);
|
|
180
|
+
const currentTarget = snapshotCurrentEntryAtPath(source, state, to, cache);
|
|
181
|
+
if (currentTarget === null) throw new Error(`Cannot write copy change record for missing path: ${to}`);
|
|
182
|
+
state.setChangeRecord({
|
|
183
|
+
path: to,
|
|
184
|
+
previous: null,
|
|
185
|
+
current: currentTarget.object,
|
|
186
|
+
source: deriveCopySource(from, sourceRecord, source, state)
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
function snapshotBaseTree(source, treeHash, dirPath) {
|
|
190
|
+
if (dirPath === "") {
|
|
191
|
+
const cached = getCachedBaseSnapshot(source, treeHash);
|
|
192
|
+
if (cached !== null) return [...cached.entries];
|
|
193
|
+
}
|
|
194
|
+
const tree = readRepoTree(source, treeHash, dirPath);
|
|
195
|
+
const out = [];
|
|
196
|
+
for (const entry of tree.entries) {
|
|
197
|
+
const path = joinChildPath(dirPath, entry.name);
|
|
198
|
+
if (entry.mode === "40000") {
|
|
199
|
+
out.push(...snapshotBaseTree(source, entry.hash, path));
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
const object = createDiffObject(normalizeBlobMode(entry.mode), entry.hash);
|
|
203
|
+
out.push({
|
|
204
|
+
path,
|
|
205
|
+
object,
|
|
206
|
+
originSignature: buildOriginSignature(object.mode, object.hash)
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
if (dirPath === "") setCachedBaseSnapshot(source, treeHash, createBaseSnapshotView(out));
|
|
210
|
+
return out;
|
|
211
|
+
}
|
|
212
|
+
function baseSnapshotViewForTree(source, treeHash) {
|
|
213
|
+
const cached = getCachedBaseSnapshot(source, treeHash);
|
|
214
|
+
if (cached !== null) return cached;
|
|
215
|
+
const view = createBaseSnapshotView(snapshotBaseTree(source, treeHash, ""));
|
|
216
|
+
setCachedBaseSnapshot(source, treeHash, view);
|
|
217
|
+
return view;
|
|
218
|
+
}
|
|
219
|
+
function snapshotCurrentTree(source, state, cache) {
|
|
220
|
+
const root = state.getNode("root");
|
|
221
|
+
if (root === null) throw new Error("Virtual workdir session is missing root node");
|
|
222
|
+
return snapshotCurrentNode(source, state, root, "", cache);
|
|
223
|
+
}
|
|
224
|
+
function snapshotCurrentNode(source, state, node, path, cache) {
|
|
225
|
+
if (node.state.kind === "directory") {
|
|
226
|
+
const out = [];
|
|
227
|
+
for (const child of getDirectoryChildrenView(source, state, node, path).children) {
|
|
228
|
+
const observedChild = observeListedDirectoryChild(state, path, child);
|
|
229
|
+
if (observedChild === null) continue;
|
|
230
|
+
out.push(...snapshotCurrentNode(source, state, observedChild.node, observedChild.path, cache));
|
|
231
|
+
}
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
return [snapshotCurrentLeafNode(source, {
|
|
235
|
+
path,
|
|
236
|
+
node
|
|
237
|
+
}, cache)];
|
|
238
|
+
}
|
|
239
|
+
function currentNodeHash(source, node, path, cache) {
|
|
240
|
+
if (node.state.kind === "file") {
|
|
241
|
+
if (node.state.content !== void 0) {
|
|
242
|
+
const cached = cache?.currentNodeHashes.get(node.id);
|
|
243
|
+
if (cached !== void 0) return cached;
|
|
244
|
+
const hash = hashObject("blob", node.state.content);
|
|
245
|
+
cache?.setCurrentNodeHash(node.id, hash);
|
|
246
|
+
return hash;
|
|
247
|
+
}
|
|
248
|
+
if (node.origin.kind === "repo-blob") return node.origin.hash;
|
|
249
|
+
}
|
|
250
|
+
if (node.state.kind === "symlink") {
|
|
251
|
+
if (node.state.target !== void 0) {
|
|
252
|
+
const cached = cache?.currentNodeHashes.get(node.id);
|
|
253
|
+
if (cached !== void 0) return cached;
|
|
254
|
+
const hash = hashObject("blob", node.state.target);
|
|
255
|
+
cache?.setCurrentNodeHash(node.id, hash);
|
|
256
|
+
return hash;
|
|
257
|
+
}
|
|
258
|
+
if (node.origin.kind === "repo-blob") return node.origin.hash;
|
|
259
|
+
}
|
|
260
|
+
if (node.origin.kind === "repo-blob") return hashObject("blob", readRepoBlobContent(source, node.origin.hash, path));
|
|
261
|
+
throw new Error(`Virtual workdir diff cannot resolve hash for path: ${path}`);
|
|
262
|
+
}
|
|
263
|
+
function computeChangeRecordForPath(source, state, path, previousRecord, cache) {
|
|
264
|
+
const baseEntry = baseSnapshotEntryAtPath(source, state.readBaseTree(), path);
|
|
265
|
+
const currentEntry = snapshotCurrentEntryAtPath(source, state, path, cache);
|
|
266
|
+
const preservedLineage = preserveLineageRecordForPath(path, previousRecord, currentEntry);
|
|
267
|
+
if (preservedLineage !== void 0) return preservedLineage;
|
|
268
|
+
if (baseEntry === null && currentEntry === null) return null;
|
|
269
|
+
if (baseEntry === null && currentEntry !== null) return createNormalizedChangeRecord(path, null, currentEntry.object);
|
|
270
|
+
if (baseEntry !== null && currentEntry === null) return createNormalizedChangeRecord(path, baseEntry.object, null);
|
|
271
|
+
if (baseEntry !== null && currentEntry !== null) {
|
|
272
|
+
if (isSameObject(baseEntry.object, currentEntry.object)) return null;
|
|
273
|
+
return createNormalizedChangeRecord(path, baseEntry.object, currentEntry.object);
|
|
274
|
+
}
|
|
275
|
+
throw new Error(`Unreachable change-record state at path: ${path}`);
|
|
276
|
+
}
|
|
277
|
+
function computeRenameRecordForPath(source, state, from, to, previousRecord, currentTarget) {
|
|
278
|
+
const derivedFromPrevious = deriveRenameRecordFromPreviousRecord(from, to, previousRecord, currentTarget);
|
|
279
|
+
if (derivedFromPrevious !== void 0) return derivedFromPrevious;
|
|
280
|
+
const baseEntry = baseSnapshotEntryAtPath(source, state.readBaseTree(), from);
|
|
281
|
+
if (baseEntry === null) return null;
|
|
282
|
+
return createNormalizedChangeRecord(to, baseEntry.object, currentTarget.object, createDiffSource("rename", from));
|
|
283
|
+
}
|
|
284
|
+
function deriveCopySource(from, sourceRecord, source, state) {
|
|
285
|
+
const fromRecordSource = sourceRecord?.source;
|
|
286
|
+
if (fromRecordSource !== null && fromRecordSource !== void 0) return createDiffSource("copy", fromRecordSource.path);
|
|
287
|
+
if (sourceRecord?.previous !== null || baseSnapshotEntryAtPath(source, state.readBaseTree(), from) !== null) return createDiffSource("copy", from);
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
function preserveLineageRecordForPath(path, previousRecord, currentEntry) {
|
|
291
|
+
if (previousRecord?.source?.kind === "rename" && previousRecord.previous !== null) {
|
|
292
|
+
if (currentEntry === null) return null;
|
|
293
|
+
return createNormalizedChangeRecord(path, previousRecord.previous, currentEntry.object, previousRecord.source);
|
|
294
|
+
}
|
|
295
|
+
if (previousRecord?.source?.kind === "copy") {
|
|
296
|
+
if (currentEntry === null) return null;
|
|
297
|
+
return createNormalizedChangeRecord(path, null, currentEntry.object, previousRecord.source);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
function deriveRenameRecordFromPreviousRecord(from, to, previousRecord, currentTarget) {
|
|
301
|
+
if (previousRecord === null) return;
|
|
302
|
+
if (previousRecord.current === null) return null;
|
|
303
|
+
if (previousRecord.source !== null) return createNormalizedChangeRecord(to, previousRecord.previous, currentTarget.object, previousRecord.source);
|
|
304
|
+
if (previousRecord.previous === null) return createNormalizedChangeRecord(to, null, currentTarget.object);
|
|
305
|
+
return createNormalizedChangeRecord(to, previousRecord.previous, currentTarget.object, createDiffSource("rename", from));
|
|
306
|
+
}
|
|
307
|
+
function snapshotCurrentEntryAtPath(source, state, path, cache) {
|
|
308
|
+
const resolved = resolveCurrentLeafAtPath(source, state, path);
|
|
309
|
+
if (resolved === null) return null;
|
|
310
|
+
return snapshotCurrentLeafNode(source, resolved, cache);
|
|
311
|
+
}
|
|
312
|
+
function snapshotCurrentLeafNode(source, leaf, cache) {
|
|
313
|
+
if (leaf.node.state.kind === "directory") throw new Error(`snapshotCurrentLeafNode called on directory: ${leaf.path}`);
|
|
314
|
+
const hash = currentNodeHash(source, leaf.node, leaf.path, cache);
|
|
315
|
+
const object = createDiffObject(leaf.node.state.mode, hash);
|
|
316
|
+
return {
|
|
317
|
+
path: leaf.path,
|
|
318
|
+
object,
|
|
319
|
+
originSignature: leaf.node.origin.kind === "repo-blob" ? buildOriginSignature(leaf.node.origin.mode, leaf.node.origin.hash) : null
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
function createNormalizedChangeRecord(path, previous, current, source = null) {
|
|
323
|
+
return {
|
|
324
|
+
path,
|
|
325
|
+
previous,
|
|
326
|
+
current,
|
|
327
|
+
source
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function createDiffSource(kind, path) {
|
|
331
|
+
return {
|
|
332
|
+
kind,
|
|
333
|
+
path
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
function createDiffObject(mode, hash) {
|
|
337
|
+
return {
|
|
338
|
+
kind: modeKind(mode),
|
|
339
|
+
mode,
|
|
340
|
+
hash
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
function diffChanges(previous, current) {
|
|
344
|
+
return {
|
|
345
|
+
kindChanged: previous.kind !== current.kind,
|
|
346
|
+
modeChanged: previous.mode !== current.mode,
|
|
347
|
+
contentChanged: previous.hash !== current.hash
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
function isSameObject(previous, current) {
|
|
351
|
+
return previous.kind === current.kind && previous.mode === current.mode && previous.hash === current.hash;
|
|
352
|
+
}
|
|
353
|
+
function normalizeBlobMode(mode) {
|
|
354
|
+
if (mode === "100755" || mode === "120000") return mode;
|
|
355
|
+
return "100644";
|
|
356
|
+
}
|
|
357
|
+
function buildOriginSignature(mode, hash) {
|
|
358
|
+
return `${mode}:${hash}`;
|
|
359
|
+
}
|
|
360
|
+
function getCachedBaseSnapshot(source, treeHash) {
|
|
361
|
+
return baseSnapshotCache.get(source)?.get(treeHash) ?? null;
|
|
362
|
+
}
|
|
363
|
+
function setCachedBaseSnapshot(source, treeHash, view) {
|
|
364
|
+
const cache = baseSnapshotCache.get(source) ?? /* @__PURE__ */ new Map();
|
|
365
|
+
cache.set(treeHash, view);
|
|
366
|
+
baseSnapshotCache.set(source, cache);
|
|
367
|
+
}
|
|
368
|
+
function createBaseSnapshotView(entries) {
|
|
369
|
+
return {
|
|
370
|
+
entries,
|
|
371
|
+
byPath: new Map(entries.map((entry) => [entry.path, entry]))
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function baseSnapshotEntryAtPath(source, treeHash, path) {
|
|
375
|
+
return baseSnapshotViewForTree(source, treeHash).byPath.get(path) ?? null;
|
|
376
|
+
}
|
|
377
|
+
function indexDeletesBySignature(deletes) {
|
|
378
|
+
const out = /* @__PURE__ */ new Map();
|
|
379
|
+
for (const entry of deletes.values()) {
|
|
380
|
+
if (entry.previous === null) continue;
|
|
381
|
+
const signature = buildOriginSignature(entry.previous.mode, entry.previous.hash);
|
|
382
|
+
const list = out.get(signature) ?? [];
|
|
383
|
+
list.push(entry);
|
|
384
|
+
out.set(signature, list);
|
|
385
|
+
}
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
function findCopySource(baseEntries, current) {
|
|
389
|
+
if (current.originSignature === null) return null;
|
|
390
|
+
const source = baseEntries.filter((entry) => entry.originSignature === current.originSignature).sort((left, right) => left.path.localeCompare(right.path))[0];
|
|
391
|
+
if (source === void 0) return null;
|
|
392
|
+
return {
|
|
393
|
+
path: source.path,
|
|
394
|
+
entry: source
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
function modeKind(mode) {
|
|
398
|
+
return mode === "120000" ? "symlink" : "blob";
|
|
399
|
+
}
|
|
400
|
+
/**
|
|
401
|
+
* 从规范化变更索引导出当前 session 的最终 diff。
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* ```ts
|
|
405
|
+
* const diff = computeVirtualDiff(state);
|
|
406
|
+
* expect(diff.map((entry) => entry.path)).toEqual(["hello.txt"]);
|
|
407
|
+
* ```
|
|
408
|
+
*/
|
|
409
|
+
function computeVirtualDiff(state) {
|
|
410
|
+
return exportVirtualDiffFromChangeRecords(state.listChangeRecords());
|
|
411
|
+
}
|
|
412
|
+
//#endregion
|
|
413
|
+
export { computeVirtualDiff, rebuildNormalizedChangeIndex, refreshChangeRecordForPath, replaceChangeRecords, rewriteChangeRecordForRename, writeChangeRecordForCopy };
|
package/dist/workdir/core.d.mts
CHANGED
|
@@ -40,25 +40,58 @@ interface VirtualDirEntry {
|
|
|
40
40
|
readonly mode: string;
|
|
41
41
|
}
|
|
42
42
|
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* 用于 `listChanges()` 返回的变更记录。
|
|
43
|
+
* diff 中的对象描述
|
|
46
44
|
*/
|
|
47
|
-
|
|
45
|
+
interface VirtualDiffObject {
|
|
46
|
+
/** 条目种类 */
|
|
47
|
+
readonly kind: "blob" | "symlink";
|
|
48
|
+
/** Git 文件模式 */
|
|
49
|
+
readonly mode: "100644" | "100755" | "120000";
|
|
50
|
+
/** 对象哈希 */
|
|
51
|
+
readonly hash: SHA1;
|
|
52
|
+
}
|
|
48
53
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
* 由 `listChanges()` 返回,描述 session 内的单次操作。
|
|
52
|
-
* 变更记录是会话内调试/测试辅助,不保证是最小 diff。
|
|
54
|
+
* rename/copy 来源描述
|
|
53
55
|
*/
|
|
54
|
-
interface
|
|
55
|
-
/**
|
|
56
|
+
interface VirtualDiffSource {
|
|
57
|
+
/** 来源类型 */
|
|
58
|
+
readonly kind: "rename" | "copy";
|
|
59
|
+
/** 来源路径 */
|
|
56
60
|
readonly path: string;
|
|
57
|
-
/** 变更操作类型 */
|
|
58
|
-
readonly type: VirtualChangeType;
|
|
59
|
-
/** rename/copy 操作的源路径(其他操作为 undefined) */
|
|
60
|
-
readonly oldPath?: string;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* 同路径更新的变化维度
|
|
64
|
+
*/
|
|
65
|
+
interface VirtualDiffChanges {
|
|
66
|
+
/** 条目种类是否变化 */
|
|
67
|
+
readonly kindChanged: boolean;
|
|
68
|
+
/** mode 是否变化 */
|
|
69
|
+
readonly modeChanged: boolean;
|
|
70
|
+
/** 内容哈希是否变化 */
|
|
71
|
+
readonly contentChanged: boolean;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 单条 diff 条目
|
|
75
|
+
*
|
|
76
|
+
* 仅描述最终状态,不表达完整会话内操作历史。
|
|
77
|
+
*/
|
|
78
|
+
type VirtualDiffEntry = {
|
|
79
|
+
/** 新建路径 */readonly kind: "create"; /** 当前路径 */
|
|
80
|
+
readonly path: string; /** 当前对象 */
|
|
81
|
+
readonly current: VirtualDiffObject; /** rename/copy 的来源 */
|
|
82
|
+
readonly source?: VirtualDiffSource;
|
|
83
|
+
} | {
|
|
84
|
+
/** 删除路径 */readonly kind: "remove"; /** 当前路径 */
|
|
85
|
+
readonly path: string; /** 删除前对象 */
|
|
86
|
+
readonly previous: VirtualDiffObject;
|
|
87
|
+
} | {
|
|
88
|
+
/** 同路径更新 */readonly kind: "update"; /** 当前路径 */
|
|
89
|
+
readonly path: string; /** 更新前对象 */
|
|
90
|
+
readonly previous: VirtualDiffObject; /** 更新后对象 */
|
|
91
|
+
readonly current: VirtualDiffObject; /** 变化维度 */
|
|
92
|
+
readonly changes: VirtualDiffChanges; /** rename/copy 的来源 */
|
|
93
|
+
readonly source?: VirtualDiffSource;
|
|
94
|
+
};
|
|
62
95
|
/**
|
|
63
96
|
* 创建 VirtualWorkdirSession 的选项
|
|
64
97
|
*/
|
|
@@ -139,6 +172,12 @@ interface VirtualWorkdirSession {
|
|
|
139
172
|
* 对纯新建节点抛出 VirtualRevertNotSupportedError。
|
|
140
173
|
*/
|
|
141
174
|
revert(path: string): void;
|
|
175
|
+
/**
|
|
176
|
+
* 读取最终 diff
|
|
177
|
+
*
|
|
178
|
+
* 输出按路径稳定排序,仅包含文件与符号链接条目。
|
|
179
|
+
*/
|
|
180
|
+
diff(): VirtualDiffEntry[];
|
|
142
181
|
/**
|
|
143
182
|
* 导出当前 overlay 为新 tree
|
|
144
183
|
*
|
|
@@ -152,13 +191,6 @@ interface VirtualWorkdirSession {
|
|
|
152
191
|
* 丢弃全部 overlay 与变更历史。
|
|
153
192
|
*/
|
|
154
193
|
reset(baseTree: SHA1): void;
|
|
155
|
-
/**
|
|
156
|
-
* 列出会话内的变更记录
|
|
157
|
-
*
|
|
158
|
-
* 是会话内调试/测试辅助,不保证是最小 diff 引擎。
|
|
159
|
-
* 输出稳定、测试可断言即可。
|
|
160
|
-
*/
|
|
161
|
-
listChanges(): VirtualChange[];
|
|
162
194
|
}
|
|
163
195
|
/**
|
|
164
196
|
* VirtualWorkdirBackend
|
|
@@ -185,4 +217,4 @@ interface VirtualWorkdirBackend {
|
|
|
185
217
|
listSessions(): VirtualWorkdirSessionId[];
|
|
186
218
|
}
|
|
187
219
|
//#endregion
|
|
188
|
-
export { CreateVirtualWorkdirSessionOptions,
|
|
220
|
+
export { CreateVirtualWorkdirSessionOptions, VirtualDiffChanges, VirtualDiffEntry, VirtualDiffObject, VirtualDiffSource, VirtualDirEntry, VirtualEntryKind, VirtualEntryStat, VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError, VirtualWorkdirBackend, VirtualWorkdirSession, VirtualWorkdirSessionId };
|