nano-git 0.2.1 → 0.2.3
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/workdir/change-log.d.mts +25 -0
- package/dist/workdir/change-log.mjs +13 -7
- package/dist/workdir/core.d.mts +23 -1
- package/dist/workdir/file-backend.d.mts +22 -0
- package/dist/workdir/file-backend.mjs +366 -0
- package/dist/workdir/file.d.mts +2 -0
- package/dist/workdir/file.mjs +2 -0
- package/dist/workdir/ids.d.mts +7 -0
- package/dist/workdir/memory-backend.d.mts +17 -0
- package/dist/workdir/memory-backend.mjs +146 -8
- package/dist/workdir/memory.d.mts +3 -2
- package/dist/workdir/memory.mjs +3 -2
- package/dist/workdir/nodes.d.mts +57 -0
- package/dist/workdir/overlay.d.mts +14 -0
- package/dist/workdir/session-id.mjs +18 -0
- package/dist/workdir/session-internal.mjs +10 -10
- package/dist/workdir/session.d.mts +13 -1
- package/dist/workdir/session.mjs +232 -205
- package/dist/workdir/sqlite-backend.d.mts +30 -0
- package/dist/workdir/sqlite-backend.mjs +363 -0
- package/dist/workdir/sqlite.d.mts +2 -0
- package/dist/workdir/sqlite.mjs +2 -0
- package/dist/workdir/state-store.d.mts +44 -0
- package/dist/workdir/write-tree.mjs +4 -4
- package/package.json +160 -39
|
@@ -0,0 +1,25 @@
|
|
|
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 };
|
|
@@ -22,15 +22,21 @@ function createVirtualChangeLog() {
|
|
|
22
22
|
return records.slice();
|
|
23
23
|
},
|
|
24
24
|
toVirtualChanges() {
|
|
25
|
-
|
|
26
|
-
for (const record of records) {
|
|
27
|
-
const mapped = mapInternalToVirtual(record);
|
|
28
|
-
if (mapped !== null) out.push(mapped);
|
|
29
|
-
}
|
|
30
|
-
return out;
|
|
25
|
+
return mapInternalChangesToVirtualChanges(records);
|
|
31
26
|
}
|
|
32
27
|
};
|
|
33
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
|
+
}
|
|
34
40
|
function mapInternalToVirtual(record) {
|
|
35
41
|
switch (record.op) {
|
|
36
42
|
case "add": return {
|
|
@@ -60,4 +66,4 @@ function mapInternalToVirtual(record) {
|
|
|
60
66
|
}
|
|
61
67
|
}
|
|
62
68
|
//#endregion
|
|
63
|
-
export { createVirtualChangeLog };
|
|
69
|
+
export { createVirtualChangeLog, mapInternalChangesToVirtualChanges };
|
package/dist/workdir/core.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SHA1 } from "../core/types.mjs";
|
|
2
|
+
import { ObjectDatabase } from "../core/types/odb.mjs";
|
|
2
3
|
import { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError } from "../core/errors.mjs";
|
|
3
4
|
|
|
4
5
|
//#region src/workdir/core.d.ts
|
|
@@ -65,12 +66,22 @@ interface CreateVirtualWorkdirSessionOptions {
|
|
|
65
66
|
/** 基线 tree 的 SHA-1 哈希 */
|
|
66
67
|
readonly baseTree: SHA1;
|
|
67
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Virtual Workdir session 标识
|
|
71
|
+
*/
|
|
72
|
+
type VirtualWorkdirSessionId = string & {
|
|
73
|
+
readonly __brand: "VirtualWorkdirSessionId";
|
|
74
|
+
};
|
|
68
75
|
/**
|
|
69
76
|
* VirtualWorkdirSession(虚拟工作目录会话)
|
|
70
77
|
*
|
|
71
78
|
* 提供独立生命周期的可变 tree 视图,基于 `baseTree + CoW overlay` 模型。
|
|
72
79
|
* 不绑定 commit,不涉及 Git index / 真实工作目录。
|
|
73
80
|
*
|
|
81
|
+
* 当前 session 对 origin 仓库对象采用弱保证:
|
|
82
|
+
* 如果 base tree / origin blob 在后续被移除、损坏或不可读取,
|
|
83
|
+
* 相关读取、`revert()`、`writeTree()` 等操作会抛出 `VirtualOriginUnavailableError`。
|
|
84
|
+
*
|
|
74
85
|
* @example
|
|
75
86
|
* ```ts
|
|
76
87
|
* import { createMemoryRepository } from "nano-git/repository/memory";
|
|
@@ -155,12 +166,23 @@ interface VirtualWorkdirSession {
|
|
|
155
166
|
* session 内部状态存储的抽象接口。
|
|
156
167
|
* memory / file / sqlite 后端通过实现此接口来提供不同的持久化策略。
|
|
157
168
|
*
|
|
169
|
+
* `file` / `sqlite` 持久化 backend 当前都按单进程、单写者场景收口;
|
|
170
|
+
* 不承诺跨进程并发写安全,也不提供多写者协调协议。
|
|
171
|
+
*
|
|
158
172
|
* 本接口在后续 Phase 中会逐步补充完整方法签名。
|
|
159
173
|
* 当前仅为命名冻结与角色声明。
|
|
160
174
|
*/
|
|
161
175
|
interface VirtualWorkdirBackend {
|
|
162
176
|
/** 后端类型标识 */
|
|
163
177
|
readonly kind: "memory" | "file" | "sqlite";
|
|
178
|
+
/** 创建新 session 并返回其标识 */
|
|
179
|
+
createSession(options: CreateVirtualWorkdirSessionOptions): VirtualWorkdirSessionId;
|
|
180
|
+
/** 打开已存在的 session */
|
|
181
|
+
openSession(source: ObjectDatabase, sessionId: VirtualWorkdirSessionId): VirtualWorkdirSession;
|
|
182
|
+
/** 删除 session */
|
|
183
|
+
deleteSession(sessionId: VirtualWorkdirSessionId): void;
|
|
184
|
+
/** 列出当前后端中可完整恢复的 session */
|
|
185
|
+
listSessions(): VirtualWorkdirSessionId[];
|
|
164
186
|
}
|
|
165
187
|
//#endregion
|
|
166
|
-
export { CreateVirtualWorkdirSessionOptions, VirtualChange, VirtualChangeType, VirtualDirEntry, VirtualEntryKind, VirtualEntryStat, VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError, VirtualWorkdirBackend, VirtualWorkdirSession };
|
|
188
|
+
export { CreateVirtualWorkdirSessionOptions, VirtualChange, VirtualChangeType, VirtualDirEntry, VirtualEntryKind, VirtualEntryStat, VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError, VirtualWorkdirBackend, VirtualWorkdirSession, VirtualWorkdirSessionId };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { VirtualWorkdirBackend } from "./core.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/workdir/file-backend.d.ts
|
|
4
|
+
/** 创建文件系统 Virtual Workdir backend 的可选参数 */
|
|
5
|
+
interface CreateFileVirtualWorkdirBackendOptions {
|
|
6
|
+
/** session 根目录名,默认 `sessions` */
|
|
7
|
+
readonly sessionsDirName?: string;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* 创建基于文件系统目录的 Virtual Workdir backend
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* const backend = createFileVirtualWorkdirBackend("/tmp/workdirs");
|
|
15
|
+
* const sessionId = backend.createSession({ baseTree: tree });
|
|
16
|
+
* const session = backend.openSession(repo.objects, sessionId);
|
|
17
|
+
* expect(session.baseTree).toBe(tree);
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
declare function createFileVirtualWorkdirBackend(rootDir: string, options?: CreateFileVirtualWorkdirBackendOptions): VirtualWorkdirBackend;
|
|
21
|
+
//#endregion
|
|
22
|
+
export { CreateFileVirtualWorkdirBackendOptions, createFileVirtualWorkdirBackend };
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
import { createRootDirectoryNode } from "./nodes.mjs";
|
|
2
|
+
import { createVirtualWorkdirSessionId } from "./session-id.mjs";
|
|
3
|
+
import { openVirtualWorkdirSession } from "./session.mjs";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
//#region src/workdir/file-backend.ts
|
|
7
|
+
/**
|
|
8
|
+
* Virtual Workdir 文件系统 backend
|
|
9
|
+
*/
|
|
10
|
+
const FILE_WORKDIR_MANIFEST_VERSION = 1;
|
|
11
|
+
const FILE_WORKDIR_TRANSACTION_SNAPSHOT_SUFFIX = ".txn-snapshot";
|
|
12
|
+
/**
|
|
13
|
+
* 创建基于文件系统目录的 Virtual Workdir backend
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const backend = createFileVirtualWorkdirBackend("/tmp/workdirs");
|
|
18
|
+
* const sessionId = backend.createSession({ baseTree: tree });
|
|
19
|
+
* const session = backend.openSession(repo.objects, sessionId);
|
|
20
|
+
* expect(session.baseTree).toBe(tree);
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
function createFileVirtualWorkdirBackend(rootDir, options = {}) {
|
|
24
|
+
const sessionsRoot = join(rootDir, options.sessionsDirName ?? "sessions");
|
|
25
|
+
mkdirSync(sessionsRoot, { recursive: true });
|
|
26
|
+
return {
|
|
27
|
+
kind: "file",
|
|
28
|
+
createSession(options) {
|
|
29
|
+
const sessionId = createVirtualWorkdirSessionId();
|
|
30
|
+
createFileVirtualWorkdirStateStore(sessionsRoot, sessionId).reset(options.baseTree);
|
|
31
|
+
return sessionId;
|
|
32
|
+
},
|
|
33
|
+
openSession(source, sessionId) {
|
|
34
|
+
if (!hasSession(sessionsRoot, sessionId)) throw new Error(`Virtual workdir session not found: ${sessionId}`);
|
|
35
|
+
validateSessionIntegrity(sessionsRoot, sessionId);
|
|
36
|
+
return openVirtualWorkdirSession(source, createFileVirtualWorkdirStateStore(sessionsRoot, sessionId));
|
|
37
|
+
},
|
|
38
|
+
deleteSession(sessionId) {
|
|
39
|
+
if (!hasSession(sessionsRoot, sessionId)) throw new Error(`Virtual workdir session not found: ${sessionId}`);
|
|
40
|
+
rmSync(getSessionDir(sessionsRoot, sessionId), {
|
|
41
|
+
recursive: true,
|
|
42
|
+
force: true
|
|
43
|
+
});
|
|
44
|
+
},
|
|
45
|
+
listSessions() {
|
|
46
|
+
if (!existsSync(sessionsRoot)) return [];
|
|
47
|
+
return readdirSync(sessionsRoot, { withFileTypes: true }).filter((entry) => entry.isDirectory()).filter((entry) => !entry.name.endsWith(FILE_WORKDIR_TRANSACTION_SNAPSHOT_SUFFIX)).filter((entry) => existsSync(getManifestPath(join(sessionsRoot, entry.name)))).map((entry) => decodePathToken(entry.name)).filter((sessionId) => {
|
|
48
|
+
try {
|
|
49
|
+
validateSessionIntegrity(sessionsRoot, sessionId);
|
|
50
|
+
return true;
|
|
51
|
+
} catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}).sort();
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* 创建单个 session 的文件系统状态存储
|
|
60
|
+
*
|
|
61
|
+
* @example
|
|
62
|
+
* ```ts
|
|
63
|
+
* const store = createFileVirtualWorkdirStateStore("/tmp/workdirs/sessions", sessionId);
|
|
64
|
+
* expect(store.kind).toBe("file");
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
function createFileVirtualWorkdirStateStore(sessionsRoot, sessionId) {
|
|
68
|
+
const sessionDir = getSessionDir(sessionsRoot, sessionId);
|
|
69
|
+
const manifestPath = getManifestPath(sessionDir);
|
|
70
|
+
const contentDir = getContentDir(sessionDir);
|
|
71
|
+
return {
|
|
72
|
+
kind: "file",
|
|
73
|
+
transact(fn) {
|
|
74
|
+
const snapshotDir = `${sessionDir}${FILE_WORKDIR_TRANSACTION_SNAPSHOT_SUFFIX}`;
|
|
75
|
+
rmSync(snapshotDir, {
|
|
76
|
+
recursive: true,
|
|
77
|
+
force: true
|
|
78
|
+
});
|
|
79
|
+
if (existsSync(sessionDir)) copyDirectoryRecursive(sessionDir, snapshotDir);
|
|
80
|
+
try {
|
|
81
|
+
const result = fn();
|
|
82
|
+
rmSync(snapshotDir, {
|
|
83
|
+
recursive: true,
|
|
84
|
+
force: true
|
|
85
|
+
});
|
|
86
|
+
return result;
|
|
87
|
+
} catch (error) {
|
|
88
|
+
rmSync(sessionDir, {
|
|
89
|
+
recursive: true,
|
|
90
|
+
force: true
|
|
91
|
+
});
|
|
92
|
+
if (existsSync(snapshotDir)) renameSync(snapshotDir, sessionDir);
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
readBaseTree() {
|
|
97
|
+
return readManifest(manifestPath).baseTree;
|
|
98
|
+
},
|
|
99
|
+
writeBaseTree(baseTree) {
|
|
100
|
+
updateManifest(sessionDir, (manifest) => ({
|
|
101
|
+
...manifest,
|
|
102
|
+
baseTree
|
|
103
|
+
}));
|
|
104
|
+
},
|
|
105
|
+
getNode(id) {
|
|
106
|
+
const record = readManifest(manifestPath).nodes[id];
|
|
107
|
+
if (record === void 0) return null;
|
|
108
|
+
return restoreNode(record, contentDir);
|
|
109
|
+
},
|
|
110
|
+
setNode(node) {
|
|
111
|
+
updateManifest(sessionDir, (manifest) => {
|
|
112
|
+
const record = persistNode(contentDir, node);
|
|
113
|
+
return {
|
|
114
|
+
...manifest,
|
|
115
|
+
nodes: {
|
|
116
|
+
...manifest.nodes,
|
|
117
|
+
[node.id]: record
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
},
|
|
122
|
+
deleteNode(id) {
|
|
123
|
+
updateManifest(sessionDir, (manifest) => {
|
|
124
|
+
if (manifest.nodes[id] === void 0) return manifest;
|
|
125
|
+
const { [id]: _deleted, ...rest } = manifest.nodes;
|
|
126
|
+
return {
|
|
127
|
+
...manifest,
|
|
128
|
+
nodes: rest
|
|
129
|
+
};
|
|
130
|
+
});
|
|
131
|
+
},
|
|
132
|
+
appendChange(record) {
|
|
133
|
+
updateManifest(sessionDir, (manifest) => ({
|
|
134
|
+
...manifest,
|
|
135
|
+
changes: [...manifest.changes, record]
|
|
136
|
+
}));
|
|
137
|
+
},
|
|
138
|
+
listChangeRecords() {
|
|
139
|
+
return readManifest(manifestPath).changes;
|
|
140
|
+
},
|
|
141
|
+
clearChanges() {
|
|
142
|
+
updateManifest(sessionDir, (manifest) => ({
|
|
143
|
+
...manifest,
|
|
144
|
+
changes: []
|
|
145
|
+
}));
|
|
146
|
+
},
|
|
147
|
+
reset(baseTree) {
|
|
148
|
+
rmSync(sessionDir, {
|
|
149
|
+
recursive: true,
|
|
150
|
+
force: true
|
|
151
|
+
});
|
|
152
|
+
ensureSessionDirs(sessionDir, contentDir);
|
|
153
|
+
writeManifestAtomic(manifestPath, createManifest(baseTree, { [createRootDirectoryNode(baseTree).id]: serializeDirectoryNode(createRootDirectoryNode(baseTree)) }));
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function hasSession(sessionsRoot, sessionId) {
|
|
158
|
+
return existsSync(getManifestPath(getSessionDir(sessionsRoot, sessionId)));
|
|
159
|
+
}
|
|
160
|
+
function validateSessionIntegrity(sessionsRoot, sessionId) {
|
|
161
|
+
const sessionDir = getSessionDir(sessionsRoot, sessionId);
|
|
162
|
+
const manifest = readManifest(getManifestPath(sessionDir));
|
|
163
|
+
const root = manifest.nodes.root;
|
|
164
|
+
if (root === void 0) throw new Error(`Virtual workdir session is corrupted: missing root node for ${sessionId}`);
|
|
165
|
+
if (restoreNode(root, getContentDir(sessionDir)).state.kind !== "directory") throw new Error(`Virtual workdir session is corrupted: root node is not a directory for ${sessionId}`);
|
|
166
|
+
for (const record of Object.values(manifest.nodes)) restoreNode(record, getContentDir(sessionDir));
|
|
167
|
+
}
|
|
168
|
+
function getSessionDir(sessionsRoot, sessionId) {
|
|
169
|
+
return join(sessionsRoot, encodePathToken(sessionId));
|
|
170
|
+
}
|
|
171
|
+
function getManifestPath(sessionDir) {
|
|
172
|
+
return join(sessionDir, "manifest.json");
|
|
173
|
+
}
|
|
174
|
+
function getContentDir(sessionDir) {
|
|
175
|
+
return join(sessionDir, "content");
|
|
176
|
+
}
|
|
177
|
+
function getContentPath(contentDir, payloadRef) {
|
|
178
|
+
return join(contentDir, `${encodePathToken(payloadRef)}.bin`);
|
|
179
|
+
}
|
|
180
|
+
function ensureSessionDirs(sessionDir, contentDir) {
|
|
181
|
+
mkdirSync(sessionDir, { recursive: true });
|
|
182
|
+
mkdirSync(contentDir, { recursive: true });
|
|
183
|
+
}
|
|
184
|
+
function copyDirectoryRecursive(sourceDir, targetDir) {
|
|
185
|
+
mkdirSync(targetDir, { recursive: true });
|
|
186
|
+
for (const entry of readdirSync(sourceDir, { withFileTypes: true })) {
|
|
187
|
+
const sourcePath = join(sourceDir, entry.name);
|
|
188
|
+
const targetPath = join(targetDir, entry.name);
|
|
189
|
+
if (entry.isDirectory()) {
|
|
190
|
+
copyDirectoryRecursive(sourcePath, targetPath);
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
writeFileSync(targetPath, readFileSync(sourcePath));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function createManifest(baseTree, nodes) {
|
|
197
|
+
return {
|
|
198
|
+
formatVersion: FILE_WORKDIR_MANIFEST_VERSION,
|
|
199
|
+
baseTree,
|
|
200
|
+
changes: [],
|
|
201
|
+
nodes
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
function updateManifest(sessionDir, updater) {
|
|
205
|
+
const manifestPath = getManifestPath(sessionDir);
|
|
206
|
+
ensureSessionDirs(sessionDir, getContentDir(sessionDir));
|
|
207
|
+
writeManifestAtomic(manifestPath, updater(readManifest(manifestPath)));
|
|
208
|
+
}
|
|
209
|
+
function writeManifestAtomic(path, manifest) {
|
|
210
|
+
writeJsonAtomic(path, manifest);
|
|
211
|
+
}
|
|
212
|
+
function readManifest(manifestPath) {
|
|
213
|
+
if (!existsSync(manifestPath)) throw new Error(`Virtual workdir session manifest not found: ${manifestPath}`);
|
|
214
|
+
const manifest = readJson(manifestPath);
|
|
215
|
+
validateManifest(manifest);
|
|
216
|
+
return manifest;
|
|
217
|
+
}
|
|
218
|
+
function validateManifest(manifest) {
|
|
219
|
+
if (typeof manifest.baseTree !== "string" || manifest.baseTree.length === 0) throw new Error("Invalid virtual workdir manifest baseTree");
|
|
220
|
+
if (!Array.isArray(manifest.changes)) throw new Error("Invalid virtual workdir manifest changes");
|
|
221
|
+
if (typeof manifest.nodes !== "object" || manifest.nodes === null || Array.isArray(manifest.nodes)) throw new Error("Invalid virtual workdir manifest nodes");
|
|
222
|
+
if (manifest.formatVersion !== FILE_WORKDIR_MANIFEST_VERSION) throw new Error(`Unsupported virtual workdir file manifest version: expected ${FILE_WORKDIR_MANIFEST_VERSION}, got ${manifest.formatVersion}`);
|
|
223
|
+
}
|
|
224
|
+
function persistNode(contentDir, node) {
|
|
225
|
+
if (node.state.kind === "directory") return serializeDirectoryNode(node);
|
|
226
|
+
if (node.state.kind === "file") {
|
|
227
|
+
const contentRef = node.state.content === void 0 ? null : persistPayload(contentDir, node.id, node.state.content);
|
|
228
|
+
return {
|
|
229
|
+
id: node.id,
|
|
230
|
+
origin: node.origin,
|
|
231
|
+
state: {
|
|
232
|
+
kind: "file",
|
|
233
|
+
mode: node.state.mode,
|
|
234
|
+
contentRef
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
const targetRef = node.state.target === void 0 ? null : persistPayload(contentDir, node.id, node.state.target);
|
|
239
|
+
return {
|
|
240
|
+
id: node.id,
|
|
241
|
+
origin: node.origin,
|
|
242
|
+
state: {
|
|
243
|
+
kind: "symlink",
|
|
244
|
+
mode: "120000",
|
|
245
|
+
targetRef
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
function serializeDirectoryNode(node) {
|
|
250
|
+
if (node.state.kind !== "directory") throw new Error("serializeDirectoryNode: node is not a directory");
|
|
251
|
+
return {
|
|
252
|
+
id: node.id,
|
|
253
|
+
origin: node.origin,
|
|
254
|
+
state: {
|
|
255
|
+
kind: "directory",
|
|
256
|
+
overlay: {
|
|
257
|
+
addedEntries: Array.from(node.state.overlay.addedEntries.entries()),
|
|
258
|
+
deletedNames: Array.from(node.state.overlay.deletedNames.values())
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
function persistPayload(contentDir, nodeId, payload) {
|
|
264
|
+
mkdirSync(contentDir, { recursive: true });
|
|
265
|
+
const payloadRef = `${nodeId}:${Date.now()}:${Math.random().toString(36).slice(2, 10)}`;
|
|
266
|
+
writeBufferAtomic(getContentPath(contentDir, payloadRef), payload);
|
|
267
|
+
return payloadRef;
|
|
268
|
+
}
|
|
269
|
+
function restoreNode(record, contentDir) {
|
|
270
|
+
const origin = restoreOrigin(record.origin);
|
|
271
|
+
if (record.state.kind === "directory") return {
|
|
272
|
+
id: record.id,
|
|
273
|
+
origin,
|
|
274
|
+
state: {
|
|
275
|
+
kind: "directory",
|
|
276
|
+
overlay: readDirectoryOverlayRecord(record.state.overlay)
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
const rawState = record.state;
|
|
280
|
+
if (rawState.kind === "file") {
|
|
281
|
+
const mode = rawState.mode;
|
|
282
|
+
if (mode !== "100644" && mode !== "100755") throw new Error(`Invalid file workdir node state mode: ${String(mode)}`);
|
|
283
|
+
const contentRef = rawState.contentRef;
|
|
284
|
+
if (contentRef !== void 0 && contentRef !== null && typeof contentRef !== "string") throw new Error("Invalid file workdir node content ref");
|
|
285
|
+
return {
|
|
286
|
+
id: record.id,
|
|
287
|
+
origin,
|
|
288
|
+
state: {
|
|
289
|
+
kind: "file",
|
|
290
|
+
mode,
|
|
291
|
+
content: contentRef === void 0 || contentRef === null ? void 0 : readPayload(contentDir, contentRef)
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
if (rawState.kind !== "symlink") throw new Error(`Invalid file workdir node state kind: ${String(rawState.kind)}`);
|
|
296
|
+
const targetRef = rawState.targetRef;
|
|
297
|
+
if (targetRef !== void 0 && targetRef !== null && typeof targetRef !== "string") throw new Error("Invalid file workdir node target ref");
|
|
298
|
+
return {
|
|
299
|
+
id: record.id,
|
|
300
|
+
origin,
|
|
301
|
+
state: {
|
|
302
|
+
kind: "symlink",
|
|
303
|
+
mode: "120000",
|
|
304
|
+
target: targetRef === void 0 || targetRef === null ? void 0 : readPayload(contentDir, targetRef)
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
function readDirectoryOverlayRecord(value) {
|
|
309
|
+
if (!isFileDirectoryOverlayPayload(value)) throw new Error("Invalid file workdir directory overlay payload");
|
|
310
|
+
return {
|
|
311
|
+
addedEntries: new Map(value.addedEntries.map(([name, nodeId]) => [name, nodeId])),
|
|
312
|
+
deletedNames: new Set(value.deletedNames)
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function isFileDirectoryOverlayPayload(value) {
|
|
316
|
+
if (typeof value !== "object" || value === null) return false;
|
|
317
|
+
const maybe = value;
|
|
318
|
+
if (!Array.isArray(maybe.addedEntries) || !Array.isArray(maybe.deletedNames)) return false;
|
|
319
|
+
const hasValidAddedEntries = maybe.addedEntries.every((entry) => Array.isArray(entry) && entry.length === 2 && typeof entry[0] === "string" && typeof entry[1] === "string");
|
|
320
|
+
const hasValidDeletedNames = maybe.deletedNames.every((name) => typeof name === "string");
|
|
321
|
+
return hasValidAddedEntries && hasValidDeletedNames;
|
|
322
|
+
}
|
|
323
|
+
function restoreOrigin(record) {
|
|
324
|
+
const origin = record;
|
|
325
|
+
if (origin.kind === "none") return { kind: "none" };
|
|
326
|
+
if (origin.kind === "repo-tree") {
|
|
327
|
+
if (typeof origin.hash !== "string" || origin.hash.length === 0) throw new Error("Invalid file workdir node: repo-tree origin is missing hash");
|
|
328
|
+
return {
|
|
329
|
+
kind: "repo-tree",
|
|
330
|
+
hash: origin.hash
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
if (origin.kind !== "repo-blob") throw new Error(`Invalid file workdir node origin kind: ${String(origin.kind)}`);
|
|
334
|
+
if (typeof origin.hash !== "string" || origin.hash.length === 0) throw new Error("Invalid file workdir node: repo-blob origin is missing hash");
|
|
335
|
+
if (origin.mode !== "100644" && origin.mode !== "100755" && origin.mode !== "120000") throw new Error(`Invalid file workdir node origin mode: ${String(origin.mode)}`);
|
|
336
|
+
return {
|
|
337
|
+
kind: "repo-blob",
|
|
338
|
+
mode: origin.mode,
|
|
339
|
+
hash: origin.hash
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
function readPayload(contentDir, payloadRef) {
|
|
343
|
+
const path = getContentPath(contentDir, payloadRef);
|
|
344
|
+
if (!existsSync(path)) throw new Error(`Virtual workdir payload not found: ${payloadRef}`);
|
|
345
|
+
return readFileSync(path);
|
|
346
|
+
}
|
|
347
|
+
function readJson(path) {
|
|
348
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
349
|
+
}
|
|
350
|
+
function writeJsonAtomic(path, value) {
|
|
351
|
+
writeBufferAtomic(path, Buffer.from(JSON.stringify(value), "utf-8"));
|
|
352
|
+
}
|
|
353
|
+
function writeBufferAtomic(path, value) {
|
|
354
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
355
|
+
const tempPath = `${path}.tmp`;
|
|
356
|
+
writeFileSync(tempPath, value);
|
|
357
|
+
renameSync(tempPath, path);
|
|
358
|
+
}
|
|
359
|
+
function encodePathToken(value) {
|
|
360
|
+
return encodeURIComponent(value);
|
|
361
|
+
}
|
|
362
|
+
function decodePathToken(value) {
|
|
363
|
+
return decodeURIComponent(value);
|
|
364
|
+
}
|
|
365
|
+
//#endregion
|
|
366
|
+
export { createFileVirtualWorkdirBackend };
|
|
@@ -0,0 +1,17 @@
|
|
|
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 };
|