nano-git 0.2.0 → 0.2.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/dist/core/errors.d.mts +71 -1
- package/dist/core/errors.mjs +99 -1
- package/dist/errors.d.mts +2 -2
- package/dist/errors.mjs +2 -2
- package/dist/log/index.d.mts +3 -0
- package/dist/log/index.mjs +2 -0
- package/dist/log/types.d.mts +71 -0
- package/dist/log/walk.d.mts +37 -0
- package/dist/log/walk.mjs +274 -0
- package/dist/workdir/change-log.mjs +63 -0
- package/dist/workdir/core.d.mts +166 -0
- package/dist/workdir/core.mjs +2 -0
- package/dist/workdir/ids.mjs +20 -0
- package/dist/workdir/memory-backend.mjs +21 -0
- package/dist/workdir/memory.d.mts +2 -0
- package/dist/workdir/memory.mjs +2 -0
- package/dist/workdir/nodes.mjs +112 -0
- package/dist/workdir/origin.mjs +55 -0
- package/dist/workdir/overlay.mjs +95 -0
- package/dist/workdir/path.mjs +70 -0
- package/dist/workdir/session-internal.mjs +141 -0
- package/dist/workdir/session.d.mts +18 -0
- package/dist/workdir/session.mjs +380 -0
- package/dist/workdir/write-tree.mjs +97 -0
- package/package.json +9 -3
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { ObjectDatabase } from "../core/types/odb.mjs";
|
|
2
|
+
import { CreateVirtualWorkdirSessionOptions, VirtualWorkdirSession } from "./core.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/workdir/session.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* 基于 ObjectDatabase 创建 VirtualWorkdirSession
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```ts
|
|
10
|
+
* const repo = createMemoryRepository();
|
|
11
|
+
* const tree = repo.createTree([]);
|
|
12
|
+
* const session = createVirtualWorkdirSession(repo.objects, { baseTree: tree });
|
|
13
|
+
* expect(session.readdir()).toEqual([]);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
declare function createVirtualWorkdirSession(source: ObjectDatabase, options: CreateVirtualWorkdirSessionOptions): VirtualWorkdirSession;
|
|
17
|
+
//#endregion
|
|
18
|
+
export { createVirtualWorkdirSession };
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError } from "../core/errors.mjs";
|
|
2
|
+
import { VIRTUAL_ROOT_NODE_ID, createNodeId } from "./ids.mjs";
|
|
3
|
+
import { overlayBindEntry, overlayRenameEntry, overlayTombstoneEntry } from "./overlay.mjs";
|
|
4
|
+
import { cloneSessionNodeForCopy, createRootDirectoryNode, revertNodeState } from "./nodes.mjs";
|
|
5
|
+
import { createVirtualWorkdirMemoryState } from "./memory-backend.mjs";
|
|
6
|
+
import { modeToVirtualEntryKind, readRepoBlobContent } from "./origin.mjs";
|
|
7
|
+
import { assertValidVirtualPath, baseName, normalizeDirectoryPath, parentPath } from "./path.mjs";
|
|
8
|
+
import { listDirectoryChildren, resolveChild, resolvePath } from "./session-internal.mjs";
|
|
9
|
+
import { writeTreeFromSession } from "./write-tree.mjs";
|
|
10
|
+
//#region src/workdir/session.ts
|
|
11
|
+
/**
|
|
12
|
+
* VirtualWorkdirSession 行为编排
|
|
13
|
+
*
|
|
14
|
+
* Phase 5:完整文件/目录 rename 与 copy 语义。
|
|
15
|
+
*/
|
|
16
|
+
/**
|
|
17
|
+
* 基于 ObjectDatabase 创建 VirtualWorkdirSession
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* const repo = createMemoryRepository();
|
|
22
|
+
* const tree = repo.createTree([]);
|
|
23
|
+
* const session = createVirtualWorkdirSession(repo.objects, { baseTree: tree });
|
|
24
|
+
* expect(session.readdir()).toEqual([]);
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
function createVirtualWorkdirSession(source, options) {
|
|
28
|
+
return buildSessionApi(source, createVirtualWorkdirMemoryState(options.baseTree));
|
|
29
|
+
}
|
|
30
|
+
function buildSessionApi(source, state) {
|
|
31
|
+
return {
|
|
32
|
+
get baseTree() {
|
|
33
|
+
return state.baseTree;
|
|
34
|
+
},
|
|
35
|
+
exists(path) {
|
|
36
|
+
if (path === "") return true;
|
|
37
|
+
return resolvePath(source, state, path).found;
|
|
38
|
+
},
|
|
39
|
+
stat(path) {
|
|
40
|
+
if (path === "") return statDirectoryNode(getRootNode(state));
|
|
41
|
+
const resolved = resolvePath(source, state, path);
|
|
42
|
+
if (!resolved.found || resolved.node === null) return null;
|
|
43
|
+
return statNode(source, resolved.node, path);
|
|
44
|
+
},
|
|
45
|
+
readdir(dirPath) {
|
|
46
|
+
const normalized = normalizeDirectoryPath(dirPath);
|
|
47
|
+
const resolved = normalized === "" ? {
|
|
48
|
+
found: true,
|
|
49
|
+
node: getRootNode(state)
|
|
50
|
+
} : resolvePath(source, state, normalized);
|
|
51
|
+
if (!resolved.found || resolved.node === null) throw new VirtualPathNotFoundError(normalized);
|
|
52
|
+
if (resolved.node.state.kind !== "directory") throw new VirtualNotDirectoryError(normalized);
|
|
53
|
+
return listDirectoryChildren(source, state, resolved.node, normalized).map((child) => ({
|
|
54
|
+
name: child.name,
|
|
55
|
+
kind: modeToVirtualEntryKind(child.mode),
|
|
56
|
+
mode: child.mode
|
|
57
|
+
}));
|
|
58
|
+
},
|
|
59
|
+
readFile(path) {
|
|
60
|
+
assertValidVirtualPath(path);
|
|
61
|
+
const resolved = resolvePath(source, state, path);
|
|
62
|
+
if (!resolved.found || resolved.node === null) throw new VirtualPathNotFoundError(path);
|
|
63
|
+
const node = resolved.node;
|
|
64
|
+
if (node.state.kind === "file") {
|
|
65
|
+
if (node.state.content !== void 0) return node.state.content;
|
|
66
|
+
if (node.origin.kind === "repo-blob") return readRepoBlobContent(source, node.origin.hash, path);
|
|
67
|
+
throw new VirtualPathNotFoundError(path);
|
|
68
|
+
}
|
|
69
|
+
if (node.state.kind === "symlink") throw new VirtualNotFileError(path);
|
|
70
|
+
throw new VirtualNotFileError(path);
|
|
71
|
+
},
|
|
72
|
+
readLink(path) {
|
|
73
|
+
assertValidVirtualPath(path);
|
|
74
|
+
const resolved = resolvePath(source, state, path);
|
|
75
|
+
if (!resolved.found || resolved.node === null) throw new VirtualPathNotFoundError(path);
|
|
76
|
+
const node = resolved.node;
|
|
77
|
+
if (node.state.kind !== "symlink") throw new VirtualNotSymlinkError(path);
|
|
78
|
+
if (node.state.target !== void 0) return node.state.target.toString("utf-8");
|
|
79
|
+
if (node.origin.kind === "repo-blob") return readRepoBlobContent(source, node.origin.hash, path).toString("utf-8");
|
|
80
|
+
throw new VirtualPathNotFoundError(path);
|
|
81
|
+
},
|
|
82
|
+
mkdir(path) {
|
|
83
|
+
assertValidVirtualPath(path);
|
|
84
|
+
const parent = parentPath(path);
|
|
85
|
+
const name = baseName(path);
|
|
86
|
+
const parentResolved = parent !== null ? resolvePath(source, state, parent) : {
|
|
87
|
+
found: true,
|
|
88
|
+
node: getRootNode(state)
|
|
89
|
+
};
|
|
90
|
+
if (!parentResolved.found || parentResolved.node === null) throw new VirtualPathNotFoundError(parent ?? path);
|
|
91
|
+
const parentNode = parentResolved.node;
|
|
92
|
+
if (parentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(parent ?? path);
|
|
93
|
+
const existing = resolveChild(source, state, parentNode, parent ?? "", name);
|
|
94
|
+
if (existing.found && existing.node !== null) throw new VirtualPathAlreadyExistsError(path);
|
|
95
|
+
const nodeId = createNodeId();
|
|
96
|
+
const newNode = {
|
|
97
|
+
id: nodeId,
|
|
98
|
+
origin: { kind: "none" },
|
|
99
|
+
state: {
|
|
100
|
+
kind: "directory",
|
|
101
|
+
overlay: {
|
|
102
|
+
addedEntries: /* @__PURE__ */ new Map(),
|
|
103
|
+
deletedNames: /* @__PURE__ */ new Set()
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
state.nodes.set(nodeId, newNode);
|
|
108
|
+
updateParentOverlay(state, parentNode.id, overlayBindEntry(parentNode.state.overlay, name, nodeId));
|
|
109
|
+
state.changeLog.append({
|
|
110
|
+
op: "add",
|
|
111
|
+
path
|
|
112
|
+
});
|
|
113
|
+
},
|
|
114
|
+
writeFile(path, content, options) {
|
|
115
|
+
assertValidVirtualPath(path);
|
|
116
|
+
const mode = options?.mode ?? "100644";
|
|
117
|
+
const parent = parentPath(path);
|
|
118
|
+
const name = baseName(path);
|
|
119
|
+
const parentResolved = parent !== null ? resolvePath(source, state, parent) : {
|
|
120
|
+
found: true,
|
|
121
|
+
node: getRootNode(state)
|
|
122
|
+
};
|
|
123
|
+
if (!parentResolved.found || parentResolved.node === null) throw new VirtualPathNotFoundError(parent ?? path);
|
|
124
|
+
const parentNode = parentResolved.node;
|
|
125
|
+
if (parentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(parent ?? path);
|
|
126
|
+
const existing = resolveChild(source, state, parentNode, parent ?? "", name);
|
|
127
|
+
if (existing.found && existing.node !== null) {
|
|
128
|
+
if (existing.node.state.kind === "directory") throw new VirtualNotFileError(path);
|
|
129
|
+
}
|
|
130
|
+
const isNew = !existing.found || existing.node === null;
|
|
131
|
+
const nodeId = existing.found ? existing.node.id : createNodeId();
|
|
132
|
+
const fileNode = {
|
|
133
|
+
id: nodeId,
|
|
134
|
+
origin: existing.found ? existing.node.origin : { kind: "none" },
|
|
135
|
+
state: {
|
|
136
|
+
kind: "file",
|
|
137
|
+
mode,
|
|
138
|
+
content
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
state.nodes.set(nodeId, fileNode);
|
|
142
|
+
updateParentOverlay(state, parentNode.id, overlayBindEntry(parentNode.state.overlay, name, nodeId));
|
|
143
|
+
state.changeLog.append({
|
|
144
|
+
op: isNew ? "add" : "modify",
|
|
145
|
+
path
|
|
146
|
+
});
|
|
147
|
+
},
|
|
148
|
+
writeLink(path, target) {
|
|
149
|
+
assertValidVirtualPath(path);
|
|
150
|
+
const parent = parentPath(path);
|
|
151
|
+
const name = baseName(path);
|
|
152
|
+
const parentResolved = parent !== null ? resolvePath(source, state, parent) : {
|
|
153
|
+
found: true,
|
|
154
|
+
node: getRootNode(state)
|
|
155
|
+
};
|
|
156
|
+
if (!parentResolved.found || parentResolved.node === null) throw new VirtualPathNotFoundError(parent ?? path);
|
|
157
|
+
const parentNode = parentResolved.node;
|
|
158
|
+
if (parentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(parent ?? path);
|
|
159
|
+
const existing = resolveChild(source, state, parentNode, parent ?? "", name);
|
|
160
|
+
if (existing.found && existing.node !== null) {
|
|
161
|
+
if (existing.node.state.kind === "directory") throw new VirtualNotFileError(path);
|
|
162
|
+
}
|
|
163
|
+
const isNew = !existing.found || existing.node === null;
|
|
164
|
+
const nodeId = existing.found ? existing.node.id : createNodeId();
|
|
165
|
+
const linkNode = {
|
|
166
|
+
id: nodeId,
|
|
167
|
+
origin: existing.found ? existing.node.origin : { kind: "none" },
|
|
168
|
+
state: {
|
|
169
|
+
kind: "symlink",
|
|
170
|
+
mode: "120000",
|
|
171
|
+
target: Buffer.from(target)
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
state.nodes.set(nodeId, linkNode);
|
|
175
|
+
updateParentOverlay(state, parentNode.id, overlayBindEntry(parentNode.state.overlay, name, nodeId));
|
|
176
|
+
state.changeLog.append({
|
|
177
|
+
op: isNew ? "add" : "modify",
|
|
178
|
+
path
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
delete(path) {
|
|
182
|
+
assertValidVirtualPath(path);
|
|
183
|
+
const parent = parentPath(path);
|
|
184
|
+
const name = baseName(path);
|
|
185
|
+
const parentResolved = parent !== null ? resolvePath(source, state, parent) : {
|
|
186
|
+
found: true,
|
|
187
|
+
node: getRootNode(state)
|
|
188
|
+
};
|
|
189
|
+
if (!parentResolved.found || parentResolved.node === null) throw new VirtualPathNotFoundError(parent ?? path);
|
|
190
|
+
const parentNode = parentResolved.node;
|
|
191
|
+
if (parentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(parent ?? path);
|
|
192
|
+
const existing = resolveChild(source, state, parentNode, parent ?? "", name);
|
|
193
|
+
if (!existing.found || existing.node === null) throw new VirtualPathNotFoundError(path);
|
|
194
|
+
updateParentOverlay(state, parentNode.id, overlayTombstoneEntry(parentNode.state.overlay, name));
|
|
195
|
+
state.changeLog.append({
|
|
196
|
+
op: "delete",
|
|
197
|
+
path
|
|
198
|
+
});
|
|
199
|
+
},
|
|
200
|
+
rename(from, to) {
|
|
201
|
+
assertValidVirtualPath(from);
|
|
202
|
+
assertValidVirtualPath(to);
|
|
203
|
+
if (from === to) return;
|
|
204
|
+
const fromParent = parentPath(from);
|
|
205
|
+
const fromName = baseName(from);
|
|
206
|
+
const fromParentResolved = fromParent !== null ? resolvePath(source, state, fromParent) : {
|
|
207
|
+
found: true,
|
|
208
|
+
node: getRootNode(state)
|
|
209
|
+
};
|
|
210
|
+
if (!fromParentResolved.found || fromParentResolved.node === null) throw new VirtualPathNotFoundError(from);
|
|
211
|
+
const fromParentNode = fromParentResolved.node;
|
|
212
|
+
if (fromParentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(from);
|
|
213
|
+
const fromChild = resolveChild(source, state, fromParentNode, fromParent ?? "", fromName);
|
|
214
|
+
if (!fromChild.found || fromChild.node === null) throw new VirtualPathNotFoundError(from);
|
|
215
|
+
const sourceNode = fromChild.node;
|
|
216
|
+
const toParent = parentPath(to);
|
|
217
|
+
const toName = baseName(to);
|
|
218
|
+
const toParentResolved = toParent !== null ? resolvePath(source, state, toParent) : {
|
|
219
|
+
found: true,
|
|
220
|
+
node: getRootNode(state)
|
|
221
|
+
};
|
|
222
|
+
if (!toParentResolved.found || toParentResolved.node === null) throw new VirtualPathNotFoundError(to);
|
|
223
|
+
const toParentNode = toParentResolved.node;
|
|
224
|
+
if (toParentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(to);
|
|
225
|
+
const toExisting = resolveChild(source, state, toParentNode, toParent ?? "", toName);
|
|
226
|
+
if (toExisting.found && toExisting.node !== null) throw new VirtualPathAlreadyExistsError(to);
|
|
227
|
+
if (sourceNode.state.kind === "directory") {
|
|
228
|
+
const toPath = to;
|
|
229
|
+
const fromPath = from;
|
|
230
|
+
if (toPath.startsWith(fromPath + "/") || toPath === fromPath) throw new Error(`Cannot rename '${from}' to '${to}': destination is a subdirectory of source`);
|
|
231
|
+
}
|
|
232
|
+
if (fromParentNode.id === toParentNode.id) updateParentOverlay(state, fromParentNode.id, overlayRenameEntry(fromParentNode.state.overlay, fromName, toName, sourceNode.id));
|
|
233
|
+
else {
|
|
234
|
+
updateParentOverlay(state, fromParentNode.id, overlayTombstoneEntry(fromParentNode.state.overlay, fromName));
|
|
235
|
+
updateParentOverlay(state, toParentNode.id, overlayBindEntry(toParentNode.state.overlay, toName, sourceNode.id));
|
|
236
|
+
}
|
|
237
|
+
state.changeLog.append({
|
|
238
|
+
op: "rename",
|
|
239
|
+
from,
|
|
240
|
+
to
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
copy(from, to) {
|
|
244
|
+
assertValidVirtualPath(from);
|
|
245
|
+
assertValidVirtualPath(to);
|
|
246
|
+
if (from === to) throw new VirtualPathAlreadyExistsError(to);
|
|
247
|
+
const fromParent = parentPath(from);
|
|
248
|
+
const fromName = baseName(from);
|
|
249
|
+
const fromParentResolved = fromParent !== null ? resolvePath(source, state, fromParent) : {
|
|
250
|
+
found: true,
|
|
251
|
+
node: getRootNode(state)
|
|
252
|
+
};
|
|
253
|
+
if (!fromParentResolved.found || fromParentResolved.node === null) throw new VirtualPathNotFoundError(from);
|
|
254
|
+
const fromParentNode = fromParentResolved.node;
|
|
255
|
+
if (fromParentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(from);
|
|
256
|
+
const fromChild = resolveChild(source, state, fromParentNode, fromParent ?? "", fromName);
|
|
257
|
+
if (!fromChild.found || fromChild.node === null) throw new VirtualPathNotFoundError(from);
|
|
258
|
+
const sourceNode = fromChild.node;
|
|
259
|
+
const toParent = parentPath(to);
|
|
260
|
+
const toName = baseName(to);
|
|
261
|
+
const toParentResolved = toParent !== null ? resolvePath(source, state, toParent) : {
|
|
262
|
+
found: true,
|
|
263
|
+
node: getRootNode(state)
|
|
264
|
+
};
|
|
265
|
+
if (!toParentResolved.found || toParentResolved.node === null) throw new VirtualPathNotFoundError(to);
|
|
266
|
+
const toParentNode = toParentResolved.node;
|
|
267
|
+
if (toParentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(to);
|
|
268
|
+
const toExisting = resolveChild(source, state, toParentNode, toParent ?? "", toName);
|
|
269
|
+
if (toExisting.found && toExisting.node !== null) throw new VirtualPathAlreadyExistsError(to);
|
|
270
|
+
const newNodeId = cloneNodeGraphForCopy(source, state, sourceNode, from);
|
|
271
|
+
updateParentOverlay(state, toParentNode.id, overlayBindEntry(toParentNode.state.overlay, toName, newNodeId));
|
|
272
|
+
state.changeLog.append({
|
|
273
|
+
op: "copy",
|
|
274
|
+
from,
|
|
275
|
+
to
|
|
276
|
+
});
|
|
277
|
+
},
|
|
278
|
+
revert(path) {
|
|
279
|
+
assertValidVirtualPath(path);
|
|
280
|
+
const resolved = resolvePath(source, state, path);
|
|
281
|
+
if (!resolved.found || resolved.node === null) throw new VirtualPathNotFoundError(path);
|
|
282
|
+
const node = resolved.node;
|
|
283
|
+
const reverted = revertNodeState(node);
|
|
284
|
+
if (reverted === node) throw new VirtualRevertNotSupportedError(path);
|
|
285
|
+
state.nodes.set(node.id, reverted);
|
|
286
|
+
state.changeLog.append({
|
|
287
|
+
op: "revert",
|
|
288
|
+
path
|
|
289
|
+
});
|
|
290
|
+
},
|
|
291
|
+
writeTree() {
|
|
292
|
+
return writeTreeFromSession(source, state);
|
|
293
|
+
},
|
|
294
|
+
reset(baseTree) {
|
|
295
|
+
state.baseTree = baseTree;
|
|
296
|
+
state.nodes.clear();
|
|
297
|
+
state.nodes.set(VIRTUAL_ROOT_NODE_ID, createRootDirectoryNode(baseTree));
|
|
298
|
+
state.changeLog.clear();
|
|
299
|
+
},
|
|
300
|
+
listChanges() {
|
|
301
|
+
return state.changeLog.toVirtualChanges();
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function getRootNode(state) {
|
|
306
|
+
const root = state.nodes.get(VIRTUAL_ROOT_NODE_ID);
|
|
307
|
+
if (root === void 0) throw new Error("Virtual workdir session is missing root node");
|
|
308
|
+
return root;
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* 更新父节点的 overlay(创建新节点对象替代 Map 中的旧引用)
|
|
312
|
+
*/
|
|
313
|
+
function updateParentOverlay(state, parentId, newOverlay) {
|
|
314
|
+
const parentNode = state.nodes.get(parentId);
|
|
315
|
+
if (!parentNode || parentNode.state.kind !== "directory") throw new Error("updateParentOverlay: parent is not a directory");
|
|
316
|
+
state.nodes.set(parentId, {
|
|
317
|
+
...parentNode,
|
|
318
|
+
state: {
|
|
319
|
+
...parentNode.state,
|
|
320
|
+
overlay: newOverlay
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
function statNode(source, node, path) {
|
|
325
|
+
if (node.state.kind === "directory") return statDirectoryNode(node);
|
|
326
|
+
if (node.state.kind === "symlink") {
|
|
327
|
+
const hash = node.origin.kind === "repo-blob" ? node.origin.hash : null;
|
|
328
|
+
let size = 0;
|
|
329
|
+
if (node.state.target !== void 0) size = node.state.target.length;
|
|
330
|
+
else if (hash !== null) size = readRepoBlobContent(source, hash, path).length;
|
|
331
|
+
return {
|
|
332
|
+
kind: "symlink",
|
|
333
|
+
mode: "120000",
|
|
334
|
+
size,
|
|
335
|
+
hash
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const hash = node.origin.kind === "repo-blob" ? node.origin.hash : null;
|
|
339
|
+
let size = 0;
|
|
340
|
+
if (node.state.content !== void 0) size = node.state.content.length;
|
|
341
|
+
else if (hash !== null) size = readRepoBlobContent(source, hash, path).length;
|
|
342
|
+
return {
|
|
343
|
+
kind: "blob",
|
|
344
|
+
mode: node.state.mode,
|
|
345
|
+
size,
|
|
346
|
+
hash
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
function statDirectoryNode(node) {
|
|
350
|
+
return {
|
|
351
|
+
kind: "tree",
|
|
352
|
+
mode: "40000",
|
|
353
|
+
size: 0,
|
|
354
|
+
hash: node.origin.kind === "repo-tree" ? node.origin.hash : null
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
function cloneNodeGraphForCopy(source, state, node, path) {
|
|
358
|
+
const newNodeId = createNodeId();
|
|
359
|
+
const cloned = cloneSessionNodeForCopy(node, newNodeId);
|
|
360
|
+
state.nodes.set(newNodeId, cloned);
|
|
361
|
+
if (node.state.kind !== "directory" || cloned.state.kind !== "directory") return newNodeId;
|
|
362
|
+
let overlay = cloned.state.overlay;
|
|
363
|
+
const children = listDirectoryChildren(source, state, node, path);
|
|
364
|
+
for (const child of children) {
|
|
365
|
+
const childNode = state.nodes.get(child.nodeId);
|
|
366
|
+
if (childNode === void 0) continue;
|
|
367
|
+
const clonedChildId = cloneNodeGraphForCopy(source, state, childNode, path === "" ? child.name : `${path}/${child.name}`);
|
|
368
|
+
overlay = overlayBindEntry(overlay, child.name, clonedChildId);
|
|
369
|
+
}
|
|
370
|
+
state.nodes.set(newNodeId, {
|
|
371
|
+
...cloned,
|
|
372
|
+
state: {
|
|
373
|
+
kind: "directory",
|
|
374
|
+
overlay
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
return newNodeId;
|
|
378
|
+
}
|
|
379
|
+
//#endregion
|
|
380
|
+
export { createVirtualWorkdirSession };
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { writeObject } from "../objects/raw.mjs";
|
|
2
|
+
import { isDirectoryOverlayDirty } from "./nodes.mjs";
|
|
3
|
+
import { listDirectoryChildren } from "./session-internal.mjs";
|
|
4
|
+
//#region src/workdir/write-tree.ts
|
|
5
|
+
/**
|
|
6
|
+
* Virtual Workdir overlay -> tree 最小化编译
|
|
7
|
+
*
|
|
8
|
+
* 遍历 session 的目录 overlay,将受影响的目录重写为新 tree,
|
|
9
|
+
* 未修改的 repo-backed 子树/文件尽量复用原对象哈希。
|
|
10
|
+
*
|
|
11
|
+
* writeTree() 成功后不清空 overlay,不推进 baseTree。
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* 将当前 session 状态编译为新的根 tree
|
|
15
|
+
*
|
|
16
|
+
* 只重写受 overlay 影响的目录;文件/符号链接仅在 materialized 时写新 blob。
|
|
17
|
+
* 未修改的 repo-backed 条目直接复用 origin hash。
|
|
18
|
+
*
|
|
19
|
+
* @param source - 可写对象数据库(用于写入新 blob/tree)
|
|
20
|
+
* @param state - session 内存状态
|
|
21
|
+
* @returns 新根 tree 的 SHA-1
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```ts
|
|
25
|
+
* const rootHash = writeTreeFromSession(repo.objects, state);
|
|
26
|
+
* ```
|
|
27
|
+
*/
|
|
28
|
+
function writeTreeFromSession(source, state) {
|
|
29
|
+
const root = state.nodes.get("root");
|
|
30
|
+
if (!root || root.state.kind !== "directory") throw new Error("Virtual workdir: root node is missing or not a directory");
|
|
31
|
+
return compileDirectory(source, source, state, root);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* 递归编译目录 overlay -> 新 tree
|
|
35
|
+
*
|
|
36
|
+
* 返回新 tree 的 SHA-1(若目录无任何变化则直接复用 origin hash)。
|
|
37
|
+
*/
|
|
38
|
+
function compileDirectory(writeSource, readSource, state, dirNode) {
|
|
39
|
+
if (dirNode.state.kind !== "directory") throw new Error("compileDirectory called on non-directory node");
|
|
40
|
+
const children = listDirectoryChildren(readSource, state, dirNode, "");
|
|
41
|
+
let anyChanged = false;
|
|
42
|
+
const newEntries = [];
|
|
43
|
+
for (const child of children) {
|
|
44
|
+
const node = state.nodes.get(child.nodeId);
|
|
45
|
+
if (!node) continue;
|
|
46
|
+
if (node.state.kind === "directory") {
|
|
47
|
+
const newHash = compileDirectory(writeSource, readSource, state, node);
|
|
48
|
+
if (newHash !== (node.origin.kind === "repo-tree" ? node.origin.hash : null)) anyChanged = true;
|
|
49
|
+
newEntries.push({
|
|
50
|
+
mode: "40000",
|
|
51
|
+
name: child.name,
|
|
52
|
+
hash: newHash
|
|
53
|
+
});
|
|
54
|
+
} else if (node.state.kind === "file") {
|
|
55
|
+
if (node.state.content !== void 0) {
|
|
56
|
+
const hash = writeObject(writeSource, {
|
|
57
|
+
type: "blob",
|
|
58
|
+
content: node.state.content
|
|
59
|
+
});
|
|
60
|
+
anyChanged = true;
|
|
61
|
+
newEntries.push({
|
|
62
|
+
mode: node.state.mode,
|
|
63
|
+
name: child.name,
|
|
64
|
+
hash
|
|
65
|
+
});
|
|
66
|
+
} else if (node.origin.kind === "repo-blob") newEntries.push({
|
|
67
|
+
mode: node.state.mode,
|
|
68
|
+
name: child.name,
|
|
69
|
+
hash: node.origin.hash
|
|
70
|
+
});
|
|
71
|
+
} else if (node.state.target !== void 0) {
|
|
72
|
+
const hash = writeObject(writeSource, {
|
|
73
|
+
type: "blob",
|
|
74
|
+
content: node.state.target
|
|
75
|
+
});
|
|
76
|
+
anyChanged = true;
|
|
77
|
+
newEntries.push({
|
|
78
|
+
mode: "120000",
|
|
79
|
+
name: child.name,
|
|
80
|
+
hash
|
|
81
|
+
});
|
|
82
|
+
} else if (node.origin.kind === "repo-blob") newEntries.push({
|
|
83
|
+
mode: "120000",
|
|
84
|
+
name: child.name,
|
|
85
|
+
hash: node.origin.hash
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (isDirectoryOverlayDirty(dirNode.state.overlay)) anyChanged = true;
|
|
89
|
+
if (!anyChanged && dirNode.origin.kind === "repo-tree") return dirNode.origin.hash;
|
|
90
|
+
newEntries.sort((a, b) => a.name.localeCompare(b.name));
|
|
91
|
+
return writeObject(writeSource, {
|
|
92
|
+
type: "tree",
|
|
93
|
+
entries: newEntries
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
//#endregion
|
|
97
|
+
export { writeTreeFromSession };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "nano-git",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"files": [
|
|
5
5
|
"dist"
|
|
6
6
|
],
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
"./backend/sqlite": "./src/backend/sqlite.ts",
|
|
15
15
|
"./errors": "./src/errors.ts",
|
|
16
16
|
"./hash-file": "./src/hash-file.ts",
|
|
17
|
+
"./log": "./src/log/index.ts",
|
|
17
18
|
"./objects": "./src/objects/index.ts",
|
|
18
19
|
"./odb/file": "./src/odb/file.ts",
|
|
19
20
|
"./odb/memory": "./src/odb/memory.ts",
|
|
@@ -40,7 +41,9 @@
|
|
|
40
41
|
"./transport/receive-pack": "./src/transport/receive-pack.ts",
|
|
41
42
|
"./transport/server/receive-pack": "./src/transport/server/receive-pack/index.ts",
|
|
42
43
|
"./transport/server/upload-pack": "./src/transport/server/upload-pack/index.ts",
|
|
43
|
-
"./transport/upload-pack": "./src/transport/upload-pack.ts"
|
|
44
|
+
"./transport/upload-pack": "./src/transport/upload-pack.ts",
|
|
45
|
+
"./workdir/core": "./src/workdir/core.ts",
|
|
46
|
+
"./workdir/memory": "./src/workdir/memory.ts"
|
|
44
47
|
},
|
|
45
48
|
"publishConfig": {
|
|
46
49
|
"exports": {
|
|
@@ -51,6 +54,7 @@
|
|
|
51
54
|
"./backend/sqlite": "./dist/backend/sqlite.mjs",
|
|
52
55
|
"./errors": "./dist/errors.mjs",
|
|
53
56
|
"./hash-file": "./dist/hash-file.mjs",
|
|
57
|
+
"./log": "./dist/log/index.mjs",
|
|
54
58
|
"./objects": "./dist/objects/index.mjs",
|
|
55
59
|
"./odb/file": "./dist/odb/file.mjs",
|
|
56
60
|
"./odb/memory": "./dist/odb/memory.mjs",
|
|
@@ -77,7 +81,9 @@
|
|
|
77
81
|
"./transport/receive-pack": "./dist/transport/receive-pack.mjs",
|
|
78
82
|
"./transport/server/receive-pack": "./dist/transport/server/receive-pack/index.mjs",
|
|
79
83
|
"./transport/server/upload-pack": "./dist/transport/server/upload-pack/index.mjs",
|
|
80
|
-
"./transport/upload-pack": "./dist/transport/upload-pack.mjs"
|
|
84
|
+
"./transport/upload-pack": "./dist/transport/upload-pack.mjs",
|
|
85
|
+
"./workdir/core": "./dist/workdir/core.mjs",
|
|
86
|
+
"./workdir/memory": "./dist/workdir/memory.mjs"
|
|
81
87
|
}
|
|
82
88
|
},
|
|
83
89
|
"scripts": {
|