nano-git 0.3.0 → 0.3.2

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.
Files changed (39) hide show
  1. package/README.md +6 -0
  2. package/dist/workdir/change-index-plan.mjs +76 -0
  3. package/dist/workdir/change-index.d.mts +21 -0
  4. package/dist/workdir/change-index.mjs +413 -0
  5. package/dist/workdir/core.d.mts +64 -63
  6. package/dist/workdir/directory-view.mjs +197 -0
  7. package/dist/workdir/dirty-dir-plan.mjs +73 -0
  8. package/dist/workdir/dirty-dir.d.mts +28 -0
  9. package/dist/workdir/dirty-dir.mjs +32 -0
  10. package/dist/workdir/file-backend.d.mts +34 -12
  11. package/dist/workdir/file-backend.mjs +161 -94
  12. package/dist/workdir/file.d.mts +2 -2
  13. package/dist/workdir/file.mjs +2 -2
  14. package/dist/workdir/ids.d.mts +1 -1
  15. package/dist/workdir/ids.mjs +3 -3
  16. package/dist/workdir/memory-backend.mjs +34 -50
  17. package/dist/workdir/memory.d.mts +2 -3
  18. package/dist/workdir/memory.mjs +2 -3
  19. package/dist/workdir/nodes.d.mts +7 -7
  20. package/dist/workdir/nodes.mjs +3 -9
  21. package/dist/workdir/overlay.d.mts +3 -3
  22. package/dist/workdir/sqlite-backend.d.mts +31 -14
  23. package/dist/workdir/sqlite-backend.mjs +238 -142
  24. package/dist/workdir/sqlite.d.mts +2 -2
  25. package/dist/workdir/sqlite.mjs +2 -2
  26. package/dist/workdir/state-store.d.mts +23 -12
  27. package/dist/workdir/workdir-path.mjs +348 -0
  28. package/dist/workdir/workdir-transaction.mjs +115 -0
  29. package/dist/workdir/workdir.d.mts +30 -0
  30. package/dist/workdir/workdir.mjs +280 -0
  31. package/dist/workdir/write-tree.mjs +108 -44
  32. package/package.json +2 -1
  33. package/dist/workdir/change-log.d.mts +0 -25
  34. package/dist/workdir/change-log.mjs +0 -69
  35. package/dist/workdir/memory-backend.d.mts +0 -17
  36. package/dist/workdir/session-id.mjs +0 -18
  37. package/dist/workdir/session-internal.mjs +0 -141
  38. package/dist/workdir/session.d.mts +0 -30
  39. package/dist/workdir/session.mjs +0 -407
package/README.md CHANGED
@@ -347,6 +347,12 @@ console.log(obj.type); // => "blob"
347
347
  bun run examples/demo.ts
348
348
  ```
349
349
 
350
+ 运行 Virtual Workdir diff 基准:
351
+
352
+ ```bash
353
+ bun run bench:workdir-diff
354
+ ```
355
+
350
356
  演示脚本展示了:
351
357
 
352
358
  - SHA-1 哈希计算
@@ -0,0 +1,76 @@
1
+ import { resolvePath } from "./workdir-path.mjs";
2
+ //#region src/workdir/change-index-plan.ts
3
+ /**
4
+ * Virtual Workdir change-index 刷新策略
5
+ *
6
+ * 把 workdir 写路径里的“是否允许增量刷新”判断与
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 "./workdir-path.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
+ * 重建当前 workdir 的规范化变更索引。
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
+ * workdir-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 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
+ * 从规范化变更索引导出当前 workdir 的最终 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 };