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.
- 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 +64 -63
- 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.d.mts +34 -12
- package/dist/workdir/file-backend.mjs +161 -94
- package/dist/workdir/file.d.mts +2 -2
- package/dist/workdir/file.mjs +2 -2
- package/dist/workdir/ids.d.mts +1 -1
- package/dist/workdir/ids.mjs +3 -3
- package/dist/workdir/memory-backend.mjs +34 -50
- package/dist/workdir/memory.d.mts +2 -3
- package/dist/workdir/memory.mjs +2 -3
- package/dist/workdir/nodes.d.mts +7 -7
- package/dist/workdir/nodes.mjs +3 -9
- package/dist/workdir/overlay.d.mts +3 -3
- package/dist/workdir/sqlite-backend.d.mts +31 -14
- package/dist/workdir/sqlite-backend.mjs +238 -142
- package/dist/workdir/sqlite.d.mts +2 -2
- package/dist/workdir/sqlite.mjs +2 -2
- package/dist/workdir/state-store.d.mts +23 -12
- package/dist/workdir/workdir-path.mjs +348 -0
- package/dist/workdir/workdir-transaction.mjs +115 -0
- package/dist/workdir/workdir.d.mts +30 -0
- package/dist/workdir/workdir.mjs +280 -0
- package/dist/workdir/write-tree.mjs +108 -44
- package/package.json +2 -1
- package/dist/workdir/change-log.d.mts +0 -25
- package/dist/workdir/change-log.mjs +0 -69
- package/dist/workdir/memory-backend.d.mts +0 -17
- package/dist/workdir/session-id.mjs +0 -18
- package/dist/workdir/session-internal.mjs +0 -141
- package/dist/workdir/session.d.mts +0 -30
- package/dist/workdir/session.mjs +0 -407
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError } from "../core/errors.mjs";
|
|
2
|
+
import { createNodeId } from "./ids.mjs";
|
|
3
|
+
import { modeToVirtualEntryKind, readRepoBlobContent } from "./origin.mjs";
|
|
4
|
+
import { overlayBindEntry, overlayRenameEntry, overlayTombstoneEntry } from "./overlay.mjs";
|
|
5
|
+
import { assertValidVirtualPath, normalizeDirectoryPath } from "./path.mjs";
|
|
6
|
+
import { getDirectoryChildrenView, getRootNode, requireExistingWriteTarget, requireMissingWriteTarget, resolveLeafWriteTarget, resolvePath, resolveWriteTransfer } from "./workdir-path.mjs";
|
|
7
|
+
import { createChangeIndexPlanner } from "./change-index-plan.mjs";
|
|
8
|
+
import { computeVirtualDiff, rebuildNormalizedChangeIndex, refreshChangeRecordForPath, replaceChangeRecords, rewriteChangeRecordForRename, writeChangeRecordForCopy } from "./change-index.mjs";
|
|
9
|
+
import { createDirtyDirPlanner } from "./dirty-dir-plan.mjs";
|
|
10
|
+
import { revertNodeState } from "./nodes.mjs";
|
|
11
|
+
import { createVirtualWorkdirMemoryStateStore } from "./memory-backend.mjs";
|
|
12
|
+
import { cloneNodeGraphForCopy, runInWriteTransaction, statDirectoryNode, statNode, updateParentOverlay } from "./workdir-transaction.mjs";
|
|
13
|
+
import { writeTreeFromSession } from "./write-tree.mjs";
|
|
14
|
+
//#region src/workdir/workdir.ts
|
|
15
|
+
/**
|
|
16
|
+
* VirtualWorkdir 行为编排
|
|
17
|
+
*
|
|
18
|
+
* Phase 5:完整文件/目录 rename 与 copy 语义。
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* 基于 ObjectDatabase 创建 VirtualWorkdir
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const repo = createMemoryRepository();
|
|
26
|
+
* const tree = repo.createTree([]);
|
|
27
|
+
* const workdir = createVirtualWorkdir(repo.objects, { baseTree: tree });
|
|
28
|
+
* expect(workdir.readdir()).toEqual([]);
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
function createVirtualWorkdir(source, options) {
|
|
32
|
+
return openVirtualWorkdir(source, createVirtualWorkdirMemoryStateStore(options.baseTree));
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* 基于已有状态存储打开 VirtualWorkdir
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```ts
|
|
39
|
+
* const store = createVirtualWorkdirMemoryStateStore(tree);
|
|
40
|
+
* const workdir = openVirtualWorkdir(repo.objects, store);
|
|
41
|
+
* expect(workdir.baseTree).toBe(tree);
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
function openVirtualWorkdir(source, state) {
|
|
45
|
+
const currentNodeHashes = /* @__PURE__ */ new Map();
|
|
46
|
+
const invalidateDiffCaches = () => {
|
|
47
|
+
currentNodeHashes.clear();
|
|
48
|
+
};
|
|
49
|
+
const refreshChangeIndex = () => {
|
|
50
|
+
invalidateDiffCaches();
|
|
51
|
+
replaceChangeRecords(state, rebuildNormalizedChangeIndex(source, state, {
|
|
52
|
+
currentNodeHashes,
|
|
53
|
+
setCurrentNodeHash(nodeId, hash) {
|
|
54
|
+
currentNodeHashes.set(nodeId, hash);
|
|
55
|
+
}
|
|
56
|
+
}));
|
|
57
|
+
};
|
|
58
|
+
const refreshChangeIndexForPath = (path) => {
|
|
59
|
+
invalidateDiffCaches();
|
|
60
|
+
refreshChangeRecordForPath(source, state, path, {
|
|
61
|
+
currentNodeHashes,
|
|
62
|
+
setCurrentNodeHash(nodeId, hash) {
|
|
63
|
+
currentNodeHashes.set(nodeId, hash);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
};
|
|
67
|
+
const rewriteChangeIndexForRename = (from, to) => {
|
|
68
|
+
invalidateDiffCaches();
|
|
69
|
+
rewriteChangeRecordForRename(source, state, from, to, {
|
|
70
|
+
currentNodeHashes,
|
|
71
|
+
setCurrentNodeHash(nodeId, hash) {
|
|
72
|
+
currentNodeHashes.set(nodeId, hash);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
const writeChangeIndexForCopy = (from, to) => {
|
|
77
|
+
invalidateDiffCaches();
|
|
78
|
+
writeChangeRecordForCopy(source, state, from, to, {
|
|
79
|
+
currentNodeHashes,
|
|
80
|
+
setCurrentNodeHash(nodeId, hash) {
|
|
81
|
+
currentNodeHashes.set(nodeId, hash);
|
|
82
|
+
}
|
|
83
|
+
});
|
|
84
|
+
};
|
|
85
|
+
const changeIndexPlanner = createChangeIndexPlanner(source, state, {
|
|
86
|
+
rebuildAll: refreshChangeIndex,
|
|
87
|
+
refreshPath: refreshChangeIndexForPath,
|
|
88
|
+
rewriteRename: rewriteChangeIndexForRename,
|
|
89
|
+
writeCopy: writeChangeIndexForCopy
|
|
90
|
+
});
|
|
91
|
+
const dirtyDirPlanner = createDirtyDirPlanner(source, state);
|
|
92
|
+
refreshChangeIndex();
|
|
93
|
+
return {
|
|
94
|
+
get baseTree() {
|
|
95
|
+
return state.readBaseTree();
|
|
96
|
+
},
|
|
97
|
+
exists(path) {
|
|
98
|
+
if (path === "") return true;
|
|
99
|
+
return resolvePath(source, state, path).found;
|
|
100
|
+
},
|
|
101
|
+
stat(path) {
|
|
102
|
+
if (path === "") return statDirectoryNode(getRootNode(state));
|
|
103
|
+
const resolved = resolvePath(source, state, path);
|
|
104
|
+
if (!resolved.found || resolved.node === null) return null;
|
|
105
|
+
return statNode(source, resolved.node, path);
|
|
106
|
+
},
|
|
107
|
+
readdir(dirPath) {
|
|
108
|
+
const normalized = normalizeDirectoryPath(dirPath);
|
|
109
|
+
const resolved = normalized === "" ? {
|
|
110
|
+
found: true,
|
|
111
|
+
node: getRootNode(state)
|
|
112
|
+
} : resolvePath(source, state, normalized);
|
|
113
|
+
if (!resolved.found || resolved.node === null) throw new VirtualPathNotFoundError(normalized);
|
|
114
|
+
if (resolved.node.state.kind !== "directory") throw new VirtualNotDirectoryError(normalized);
|
|
115
|
+
return getDirectoryChildrenView(source, state, resolved.node, normalized).children.map((child) => ({
|
|
116
|
+
name: child.name,
|
|
117
|
+
kind: modeToVirtualEntryKind(child.mode),
|
|
118
|
+
mode: child.mode
|
|
119
|
+
}));
|
|
120
|
+
},
|
|
121
|
+
readFile(path) {
|
|
122
|
+
assertValidVirtualPath(path);
|
|
123
|
+
const resolved = resolvePath(source, state, path);
|
|
124
|
+
if (!resolved.found || resolved.node === null) throw new VirtualPathNotFoundError(path);
|
|
125
|
+
const node = resolved.node;
|
|
126
|
+
if (node.state.kind === "file") {
|
|
127
|
+
if (node.state.content !== void 0) return node.state.content;
|
|
128
|
+
if (node.origin.kind === "repo-blob") return readRepoBlobContent(source, node.origin.hash, path);
|
|
129
|
+
throw new VirtualPathNotFoundError(path);
|
|
130
|
+
}
|
|
131
|
+
if (node.state.kind === "symlink") throw new VirtualNotFileError(path);
|
|
132
|
+
throw new VirtualNotFileError(path);
|
|
133
|
+
},
|
|
134
|
+
readLink(path) {
|
|
135
|
+
assertValidVirtualPath(path);
|
|
136
|
+
const resolved = resolvePath(source, state, path);
|
|
137
|
+
if (!resolved.found || resolved.node === null) throw new VirtualPathNotFoundError(path);
|
|
138
|
+
const node = resolved.node;
|
|
139
|
+
if (node.state.kind !== "symlink") throw new VirtualNotSymlinkError(path);
|
|
140
|
+
if (node.state.target !== void 0) return node.state.target.toString("utf-8");
|
|
141
|
+
if (node.origin.kind === "repo-blob") return readRepoBlobContent(source, node.origin.hash, path).toString("utf-8");
|
|
142
|
+
throw new VirtualPathNotFoundError(path);
|
|
143
|
+
},
|
|
144
|
+
mkdir(path) {
|
|
145
|
+
runInWriteTransaction(state, () => dirtyDirPlanner.rebuild([path]), invalidateDiffCaches, () => {
|
|
146
|
+
const target = requireMissingWriteTarget(source, state, path);
|
|
147
|
+
const nodeId = createNodeId();
|
|
148
|
+
const newNode = {
|
|
149
|
+
id: nodeId,
|
|
150
|
+
origin: { kind: "none" },
|
|
151
|
+
state: {
|
|
152
|
+
kind: "directory",
|
|
153
|
+
overlay: {
|
|
154
|
+
addedEntries: /* @__PURE__ */ new Map(),
|
|
155
|
+
deletedNames: /* @__PURE__ */ new Set()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
state.setNode(newNode);
|
|
160
|
+
updateParentOverlay(state, target.parentNode.id, overlayBindEntry(target.parentNode.state.overlay, target.name, nodeId));
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
writeFile(path, content, options) {
|
|
164
|
+
runInWriteTransaction(state, () => {
|
|
165
|
+
changeIndexPlanner.apply(changeIndexPlanner.planRefreshForPath(path));
|
|
166
|
+
dirtyDirPlanner.rebuild([path]);
|
|
167
|
+
}, invalidateDiffCaches, () => {
|
|
168
|
+
const mode = options?.mode ?? "100644";
|
|
169
|
+
const target = resolveLeafWriteTarget(source, state, path);
|
|
170
|
+
const nodeId = target.existing !== null ? target.existing.node.id : createNodeId();
|
|
171
|
+
const fileNode = {
|
|
172
|
+
id: nodeId,
|
|
173
|
+
origin: target.existing !== null ? target.existing.node.origin : { kind: "none" },
|
|
174
|
+
state: {
|
|
175
|
+
kind: "file",
|
|
176
|
+
mode,
|
|
177
|
+
content
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
state.setNode(fileNode);
|
|
181
|
+
if (target.existing === null || target.parentNode.state.overlay.addedEntries.has(target.name)) updateParentOverlay(state, target.parentNode.id, overlayBindEntry(target.parentNode.state.overlay, target.name, nodeId));
|
|
182
|
+
});
|
|
183
|
+
},
|
|
184
|
+
writeLink(path, target) {
|
|
185
|
+
runInWriteTransaction(state, () => {
|
|
186
|
+
changeIndexPlanner.apply(changeIndexPlanner.planRefreshForPath(path));
|
|
187
|
+
dirtyDirPlanner.rebuild([path]);
|
|
188
|
+
}, invalidateDiffCaches, () => {
|
|
189
|
+
const writeTarget = resolveLeafWriteTarget(source, state, path);
|
|
190
|
+
const nodeId = writeTarget.existing !== null ? writeTarget.existing.node.id : createNodeId();
|
|
191
|
+
const linkNode = {
|
|
192
|
+
id: nodeId,
|
|
193
|
+
origin: writeTarget.existing !== null ? writeTarget.existing.node.origin : { kind: "none" },
|
|
194
|
+
state: {
|
|
195
|
+
kind: "symlink",
|
|
196
|
+
mode: "120000",
|
|
197
|
+
target: Buffer.from(target)
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
state.setNode(linkNode);
|
|
201
|
+
if (writeTarget.existing === null || writeTarget.parentNode.state.overlay.addedEntries.has(writeTarget.name)) updateParentOverlay(state, writeTarget.parentNode.id, overlayBindEntry(writeTarget.parentNode.state.overlay, writeTarget.name, nodeId));
|
|
202
|
+
});
|
|
203
|
+
},
|
|
204
|
+
delete(path) {
|
|
205
|
+
runInWriteTransaction(state, () => {
|
|
206
|
+
changeIndexPlanner.apply(changeIndexPlanner.planRefreshForPath(path, { treatMissingAsIncremental: true }));
|
|
207
|
+
dirtyDirPlanner.rebuild([path]);
|
|
208
|
+
}, invalidateDiffCaches, () => {
|
|
209
|
+
const target = requireExistingWriteTarget(source, state, path);
|
|
210
|
+
updateParentOverlay(state, target.parentNode.id, overlayTombstoneEntry(target.parentNode.state.overlay, target.name));
|
|
211
|
+
});
|
|
212
|
+
},
|
|
213
|
+
rename(from, to) {
|
|
214
|
+
runInWriteTransaction(state, () => {
|
|
215
|
+
changeIndexPlanner.apply(changeIndexPlanner.planRewriteForRename(from, to));
|
|
216
|
+
dirtyDirPlanner.rebuild([from, to]);
|
|
217
|
+
}, invalidateDiffCaches, () => {
|
|
218
|
+
assertValidVirtualPath(from);
|
|
219
|
+
assertValidVirtualPath(to);
|
|
220
|
+
if (from === to) return;
|
|
221
|
+
const { from: fromTarget, to: toTarget } = resolveWriteTransfer(source, state, from, to);
|
|
222
|
+
const sourceNode = fromTarget.existing.node;
|
|
223
|
+
if (toTarget.existing !== null) throw new VirtualPathAlreadyExistsError(to);
|
|
224
|
+
if (sourceNode.state.kind === "directory") {
|
|
225
|
+
const toPath = to;
|
|
226
|
+
const fromPath = from;
|
|
227
|
+
if (toPath.startsWith(fromPath + "/") || toPath === fromPath) throw new Error(`Cannot rename '${from}' to '${to}': destination is a subdirectory of source`);
|
|
228
|
+
}
|
|
229
|
+
if (fromTarget.parentNode.id === toTarget.parentNode.id) updateParentOverlay(state, fromTarget.parentNode.id, overlayRenameEntry(fromTarget.parentNode.state.overlay, fromTarget.name, toTarget.name, sourceNode.id));
|
|
230
|
+
else {
|
|
231
|
+
updateParentOverlay(state, fromTarget.parentNode.id, overlayTombstoneEntry(fromTarget.parentNode.state.overlay, fromTarget.name));
|
|
232
|
+
updateParentOverlay(state, toTarget.parentNode.id, overlayBindEntry(toTarget.parentNode.state.overlay, toTarget.name, sourceNode.id));
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
},
|
|
236
|
+
copy(from, to) {
|
|
237
|
+
runInWriteTransaction(state, () => {
|
|
238
|
+
changeIndexPlanner.apply(changeIndexPlanner.planWriteForCopy(from, to));
|
|
239
|
+
dirtyDirPlanner.rebuild([from, to]);
|
|
240
|
+
}, invalidateDiffCaches, () => {
|
|
241
|
+
assertValidVirtualPath(from);
|
|
242
|
+
assertValidVirtualPath(to);
|
|
243
|
+
if (from === to) throw new VirtualPathAlreadyExistsError(to);
|
|
244
|
+
const { from: fromTarget, to: toTarget } = resolveWriteTransfer(source, state, from, to);
|
|
245
|
+
const sourceNode = fromTarget.existing.node;
|
|
246
|
+
if (toTarget.existing !== null) throw new VirtualPathAlreadyExistsError(to);
|
|
247
|
+
const newNodeId = cloneNodeGraphForCopy(source, state, sourceNode, from);
|
|
248
|
+
updateParentOverlay(state, toTarget.parentNode.id, overlayBindEntry(toTarget.parentNode.state.overlay, toTarget.name, newNodeId));
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
revert(path) {
|
|
252
|
+
runInWriteTransaction(state, () => {
|
|
253
|
+
changeIndexPlanner.apply(changeIndexPlanner.planRefreshForPath(path));
|
|
254
|
+
dirtyDirPlanner.rebuild([path]);
|
|
255
|
+
}, invalidateDiffCaches, () => {
|
|
256
|
+
assertValidVirtualPath(path);
|
|
257
|
+
const resolved = resolvePath(source, state, path);
|
|
258
|
+
if (!resolved.found || resolved.node === null) throw new VirtualPathNotFoundError(path);
|
|
259
|
+
const node = resolved.node;
|
|
260
|
+
const reverted = revertNodeState(node);
|
|
261
|
+
if (reverted === node) throw new VirtualRevertNotSupportedError(path);
|
|
262
|
+
state.setNode(reverted);
|
|
263
|
+
});
|
|
264
|
+
},
|
|
265
|
+
diff() {
|
|
266
|
+
return computeVirtualDiff(state);
|
|
267
|
+
},
|
|
268
|
+
writeTree() {
|
|
269
|
+
return writeTreeFromSession(source, state);
|
|
270
|
+
},
|
|
271
|
+
reset(baseTree) {
|
|
272
|
+
runInWriteTransaction(state, null, invalidateDiffCaches, () => {
|
|
273
|
+
state.reset(baseTree);
|
|
274
|
+
dirtyDirPlanner.clear();
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
//#endregion
|
|
280
|
+
export { createVirtualWorkdir, openVirtualWorkdir };
|
|
@@ -1,23 +1,25 @@
|
|
|
1
1
|
import { writeObject } from "../objects/raw.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { listDirectoryChildren } from "./
|
|
2
|
+
import { readRepoTree } from "./origin.mjs";
|
|
3
|
+
import { listDirectoryChildren } from "./workdir-path.mjs";
|
|
4
|
+
import { createNamedOriginChildLookup, observeListedDirectoryChild, observeNamedDirectoryChild, planAffectedDirectoryChildren } from "./directory-view.mjs";
|
|
5
|
+
import { materializeDirtyDirSummary } from "./dirty-dir.mjs";
|
|
4
6
|
//#region src/workdir/write-tree.ts
|
|
5
7
|
/**
|
|
6
8
|
* Virtual Workdir overlay -> tree 最小化编译
|
|
7
9
|
*
|
|
8
|
-
* 遍历
|
|
10
|
+
* 遍历 workdir 的目录 overlay,将受影响的目录重写为新 tree,
|
|
9
11
|
* 未修改的 repo-backed 子树/文件尽量复用原对象哈希。
|
|
10
12
|
*
|
|
11
13
|
* writeTree() 成功后不清空 overlay,不推进 baseTree。
|
|
12
14
|
*/
|
|
13
15
|
/**
|
|
14
|
-
* 将当前
|
|
16
|
+
* 将当前 workdir 状态编译为新的根 tree
|
|
15
17
|
*
|
|
16
18
|
* 只重写受 overlay 影响的目录;文件/符号链接仅在 materialized 时写新 blob。
|
|
17
19
|
* 未修改的 repo-backed 条目直接复用 origin hash。
|
|
18
20
|
*
|
|
19
21
|
* @param source - 可写对象数据库(用于写入新 blob/tree)
|
|
20
|
-
* @param state -
|
|
22
|
+
* @param state - workdir 内部状态
|
|
21
23
|
* @returns 新根 tree 的 SHA-1
|
|
22
24
|
*
|
|
23
25
|
* @example
|
|
@@ -35,63 +37,125 @@ function writeTreeFromSession(source, state) {
|
|
|
35
37
|
*
|
|
36
38
|
* 返回新 tree 的 SHA-1(若目录无任何变化则直接复用 origin hash)。
|
|
37
39
|
*/
|
|
38
|
-
function compileDirectory(writeSource, readSource, state, dirNode) {
|
|
40
|
+
function compileDirectory(writeSource, readSource, state, dirNode, dirPath = "") {
|
|
39
41
|
if (dirNode.state.kind !== "directory") throw new Error("compileDirectory called on non-directory node");
|
|
40
|
-
const
|
|
42
|
+
const summary = state.getDirtyDirSummary(dirPath);
|
|
43
|
+
if (summary !== null && summary.hashState === "materialized" && summary.currentTreeHash !== null) return summary.currentTreeHash;
|
|
44
|
+
if (summary === null && dirNode.origin.kind === "repo-tree") return dirNode.origin.hash;
|
|
41
45
|
let anyChanged = false;
|
|
42
|
-
const newEntries =
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
const newEntries = collectCompiledEntries(writeSource, readSource, state, dirNode, dirPath, summary, (changed) => {
|
|
47
|
+
if (changed) anyChanged = true;
|
|
48
|
+
});
|
|
49
|
+
if (summary !== null && (summary.dirtyEntryCount > 0 || summary.dirtyDescendantCount > 0)) anyChanged = true;
|
|
50
|
+
if (!anyChanged && dirNode.origin.kind === "repo-tree") {
|
|
51
|
+
state.setDirtyDirSummary(materializeDirtyDirSummary(summary, dirPath, dirNode.origin.hash));
|
|
52
|
+
return dirNode.origin.hash;
|
|
53
|
+
}
|
|
54
|
+
newEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
55
|
+
const treeHash = writeObject(writeSource, {
|
|
56
|
+
type: "tree",
|
|
57
|
+
entries: newEntries
|
|
58
|
+
});
|
|
59
|
+
state.setDirtyDirSummary(materializeDirtyDirSummary(summary, dirPath, treeHash));
|
|
60
|
+
return treeHash;
|
|
61
|
+
}
|
|
62
|
+
function collectCompiledEntries(writeSource, readSource, state, dirNode, dirPath, summary, markChanged) {
|
|
63
|
+
if (dirNode.state.kind !== "directory") throw new Error("collectCompiledEntries called on non-directory node");
|
|
64
|
+
if (dirNode.origin.kind !== "repo-tree" || summary === null) return listDirectoryChildren(readSource, state, dirNode, dirPath).flatMap((child) => {
|
|
65
|
+
const observedChild = observeListedDirectoryChild(state, dirPath, child);
|
|
66
|
+
if (observedChild === null) return [];
|
|
67
|
+
const entry = compileChildEntry(writeSource, readSource, state, observedChild);
|
|
68
|
+
if (entry === null) return [];
|
|
69
|
+
markChanged(entry.changed);
|
|
70
|
+
return [entry.entry];
|
|
71
|
+
});
|
|
72
|
+
const originLookup = createNamedOriginChildLookup(readRepoTree(readSource, dirNode.origin.hash, dirPath).entries);
|
|
73
|
+
const childPlan = planAffectedDirectoryChildren(originLookup, collectAffectedChildNames(summary));
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const planEntry of childPlan) {
|
|
76
|
+
if (!planEntry.shouldCompile) {
|
|
77
|
+
if (planEntry.originEntry === null) throw new Error(`collectCompiledEntries: missing origin entry for '${planEntry.name}'`);
|
|
78
|
+
out.push(planEntry.originEntry);
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const compiled = compileNamedChildEntry(writeSource, readSource, state, dirNode, dirPath, planEntry.name, originLookup);
|
|
82
|
+
if (compiled === null) continue;
|
|
83
|
+
markChanged(compiled.changed);
|
|
84
|
+
out.push(compiled.entry);
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
function collectAffectedChildNames(summary) {
|
|
89
|
+
return new Set(summary.affectedNames);
|
|
90
|
+
}
|
|
91
|
+
function compileNamedChildEntry(writeSource, readSource, state, dirNode, dirPath, name, originLookup) {
|
|
92
|
+
if (dirNode.state.kind !== "directory") throw new Error("compileNamedChildEntry called on non-directory node");
|
|
93
|
+
const observedChild = observeNamedDirectoryChild(state, dirNode, dirPath, originLookup, name);
|
|
94
|
+
if (observedChild === null) return null;
|
|
95
|
+
return compileChildEntry(writeSource, readSource, state, observedChild);
|
|
96
|
+
}
|
|
97
|
+
function compileChildEntry(writeSource, readSource, state, child) {
|
|
98
|
+
const node = child.node;
|
|
99
|
+
if (node.state.kind === "directory") {
|
|
100
|
+
const newHash = compileDirectory(writeSource, readSource, state, node, child.path);
|
|
101
|
+
const originHash = node.origin.kind === "repo-tree" ? node.origin.hash : null;
|
|
102
|
+
return {
|
|
103
|
+
entry: {
|
|
50
104
|
mode: "40000",
|
|
51
105
|
name: child.name,
|
|
52
106
|
hash: newHash
|
|
107
|
+
},
|
|
108
|
+
changed: newHash !== originHash
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (node.state.kind === "file") {
|
|
112
|
+
if (node.state.content !== void 0) {
|
|
113
|
+
const hash = writeObject(writeSource, {
|
|
114
|
+
type: "blob",
|
|
115
|
+
content: node.state.content
|
|
53
116
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const hash = writeObject(writeSource, {
|
|
57
|
-
type: "blob",
|
|
58
|
-
content: node.state.content
|
|
59
|
-
});
|
|
60
|
-
anyChanged = true;
|
|
61
|
-
newEntries.push({
|
|
117
|
+
return {
|
|
118
|
+
entry: {
|
|
62
119
|
mode: node.state.mode,
|
|
63
120
|
name: child.name,
|
|
64
121
|
hash
|
|
65
|
-
}
|
|
66
|
-
|
|
122
|
+
},
|
|
123
|
+
changed: node.origin.kind !== "repo-blob" || hash !== node.origin.hash
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (node.origin.kind === "repo-blob") return {
|
|
127
|
+
entry: {
|
|
67
128
|
mode: node.state.mode,
|
|
68
129
|
name: child.name,
|
|
69
130
|
hash: node.origin.hash
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
131
|
+
},
|
|
132
|
+
changed: false
|
|
133
|
+
};
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (node.state.target !== void 0) {
|
|
137
|
+
const hash = writeObject(writeSource, {
|
|
138
|
+
type: "blob",
|
|
139
|
+
content: node.state.target
|
|
140
|
+
});
|
|
141
|
+
return {
|
|
142
|
+
entry: {
|
|
78
143
|
mode: "120000",
|
|
79
144
|
name: child.name,
|
|
80
145
|
hash
|
|
81
|
-
}
|
|
82
|
-
|
|
146
|
+
},
|
|
147
|
+
changed: node.origin.kind !== "repo-blob" || hash !== node.origin.hash
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (node.origin.kind === "repo-blob") return {
|
|
151
|
+
entry: {
|
|
83
152
|
mode: "120000",
|
|
84
153
|
name: child.name,
|
|
85
154
|
hash: node.origin.hash
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
newEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
91
|
-
return writeObject(writeSource, {
|
|
92
|
-
type: "tree",
|
|
93
|
-
entries: newEntries
|
|
94
|
-
});
|
|
155
|
+
},
|
|
156
|
+
changed: false
|
|
157
|
+
};
|
|
158
|
+
return null;
|
|
95
159
|
}
|
|
96
160
|
//#endregion
|
|
97
161
|
export { writeTreeFromSession };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nano-git",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"files": [
|
|
5
5
|
"dist"
|
|
6
6
|
],
|
|
@@ -169,6 +169,7 @@
|
|
|
169
169
|
},
|
|
170
170
|
"scripts": {
|
|
171
171
|
"build": "tsdown",
|
|
172
|
+
"bench:workdir-diff": "bun run examples/workdir-diff-benchmark.ts",
|
|
172
173
|
"lint": "oxlint .",
|
|
173
174
|
"format": "oxfmt .",
|
|
174
175
|
"format:check": "oxfmt --check .",
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
//#region src/workdir/change-log.d.ts
|
|
2
|
-
/** 内部变更操作(含 revert,公开 API 暂不暴露 revert) */
|
|
3
|
-
type InternalChangeRecord = {
|
|
4
|
-
readonly op: "add";
|
|
5
|
-
readonly path: string;
|
|
6
|
-
} | {
|
|
7
|
-
readonly op: "modify";
|
|
8
|
-
readonly path: string;
|
|
9
|
-
} | {
|
|
10
|
-
readonly op: "delete";
|
|
11
|
-
readonly path: string;
|
|
12
|
-
} | {
|
|
13
|
-
readonly op: "rename";
|
|
14
|
-
readonly from: string;
|
|
15
|
-
readonly to: string;
|
|
16
|
-
} | {
|
|
17
|
-
readonly op: "copy";
|
|
18
|
-
readonly from: string;
|
|
19
|
-
readonly to: string;
|
|
20
|
-
} | {
|
|
21
|
-
readonly op: "revert";
|
|
22
|
-
readonly path: string;
|
|
23
|
-
};
|
|
24
|
-
//#endregion
|
|
25
|
-
export { InternalChangeRecord };
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
//#region src/workdir/change-log.ts
|
|
2
|
-
/**
|
|
3
|
-
* 创建空的变更日志
|
|
4
|
-
*
|
|
5
|
-
* @example
|
|
6
|
-
* ```ts
|
|
7
|
-
* const log = createVirtualChangeLog();
|
|
8
|
-
* log.append({ op: "add", path: "a.txt" });
|
|
9
|
-
* expect(log.toVirtualChanges()).toEqual([{ path: "a.txt", type: "add" }]);
|
|
10
|
-
* ```
|
|
11
|
-
*/
|
|
12
|
-
function createVirtualChangeLog() {
|
|
13
|
-
const records = [];
|
|
14
|
-
return {
|
|
15
|
-
append(record) {
|
|
16
|
-
records.push(record);
|
|
17
|
-
},
|
|
18
|
-
clear() {
|
|
19
|
-
records.length = 0;
|
|
20
|
-
},
|
|
21
|
-
snapshot() {
|
|
22
|
-
return records.slice();
|
|
23
|
-
},
|
|
24
|
-
toVirtualChanges() {
|
|
25
|
-
return mapInternalChangesToVirtualChanges(records);
|
|
26
|
-
}
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
/**
|
|
30
|
-
* 将内部变更记录映射为公开 VirtualChange 列表
|
|
31
|
-
*/
|
|
32
|
-
function mapInternalChangesToVirtualChanges(records) {
|
|
33
|
-
const out = [];
|
|
34
|
-
for (const record of records) {
|
|
35
|
-
const mapped = mapInternalToVirtual(record);
|
|
36
|
-
if (mapped !== null) out.push(mapped);
|
|
37
|
-
}
|
|
38
|
-
return out;
|
|
39
|
-
}
|
|
40
|
-
function mapInternalToVirtual(record) {
|
|
41
|
-
switch (record.op) {
|
|
42
|
-
case "add": return {
|
|
43
|
-
path: record.path,
|
|
44
|
-
type: "add"
|
|
45
|
-
};
|
|
46
|
-
case "modify": return {
|
|
47
|
-
path: record.path,
|
|
48
|
-
type: "modify"
|
|
49
|
-
};
|
|
50
|
-
case "delete": return {
|
|
51
|
-
path: record.path,
|
|
52
|
-
type: "delete"
|
|
53
|
-
};
|
|
54
|
-
case "rename": return {
|
|
55
|
-
path: record.to,
|
|
56
|
-
type: "rename",
|
|
57
|
-
oldPath: record.from
|
|
58
|
-
};
|
|
59
|
-
case "copy": return {
|
|
60
|
-
path: record.to,
|
|
61
|
-
type: "copy",
|
|
62
|
-
oldPath: record.from
|
|
63
|
-
};
|
|
64
|
-
case "revert": return null;
|
|
65
|
-
default: return record;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
//#endregion
|
|
69
|
-
export { createVirtualChangeLog, mapInternalChangesToVirtualChanges };
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { VirtualWorkdirBackend } from "./core.mjs";
|
|
2
|
-
|
|
3
|
-
//#region src/workdir/memory-backend.d.ts
|
|
4
|
-
/**
|
|
5
|
-
* 创建内存版 Virtual Workdir backend
|
|
6
|
-
*
|
|
7
|
-
* @example
|
|
8
|
-
* ```ts
|
|
9
|
-
* const backend = createMemoryVirtualWorkdirBackend();
|
|
10
|
-
* const sessionId = backend.createSession({ baseTree: tree });
|
|
11
|
-
* const session = backend.openSession(repo.objects, sessionId);
|
|
12
|
-
* expect(session.baseTree).toBe(tree);
|
|
13
|
-
* ```
|
|
14
|
-
*/
|
|
15
|
-
declare function createMemoryVirtualWorkdirBackend(): VirtualWorkdirBackend;
|
|
16
|
-
//#endregion
|
|
17
|
-
export { createMemoryVirtualWorkdirBackend };
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
//#region src/workdir/session-id.ts
|
|
2
|
-
let nextSessionCounter = 1;
|
|
3
|
-
/**
|
|
4
|
-
* 分配新的 session ID
|
|
5
|
-
*
|
|
6
|
-
* @example
|
|
7
|
-
* ```ts
|
|
8
|
-
* const sessionId = createVirtualWorkdirSessionId();
|
|
9
|
-
* expect(String(sessionId).startsWith("session:")).toBe(true);
|
|
10
|
-
* ```
|
|
11
|
-
*/
|
|
12
|
-
function createVirtualWorkdirSessionId() {
|
|
13
|
-
const id = `session:${nextSessionCounter}`;
|
|
14
|
-
nextSessionCounter += 1;
|
|
15
|
-
return id;
|
|
16
|
-
}
|
|
17
|
-
//#endregion
|
|
18
|
-
export { createVirtualWorkdirSessionId };
|