nano-git 0.1.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/README.md +58 -1
- package/dist/backend/sqlite.d.mts +44 -0
- package/dist/backend/sqlite.mjs +59 -0
- 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/index.d.mts +2 -1
- 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/odb/sqlite.d.mts +23 -0
- package/dist/odb/sqlite.mjs +83 -0
- package/dist/refs/shallow/sqlite.d.mts +23 -0
- package/dist/refs/shallow/sqlite.mjs +61 -0
- package/dist/refs/sqlite.d.mts +23 -0
- package/dist/refs/sqlite.mjs +135 -0
- package/dist/remote/http.d.mts +51 -0
- package/dist/remote/http.mjs +74 -0
- package/dist/remote/types.d.mts +20 -0
- package/dist/repository/import/import-session-types.d.mts +3 -16
- package/dist/repository/ops/object-operations.mjs +0 -5
- package/dist/repository/ops/object-types.d.mts +0 -21
- package/dist/repository/sqlite.d.mts +28 -0
- package/dist/repository/sqlite.mjs +45 -0
- package/dist/transport/upload-pack.d.mts +1 -1
- package/dist/transport/upload-pack.mjs +1 -1
- 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 +21 -3
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createV2HttpTransport } from "../transport/client/upload-pack/http.mjs";
|
|
2
|
+
import { lsRefs, lsRefsToRefAdvertisement } from "../transport/client/upload-pack/ls-refs.mjs";
|
|
3
|
+
import { objectInfo } from "../transport/client/upload-pack/object-info.mjs";
|
|
4
|
+
//#region src/remote/http.ts
|
|
5
|
+
/**
|
|
6
|
+
* 基于 HTTP 的远端 Git 查询 API
|
|
7
|
+
*
|
|
8
|
+
* 将纯远端查询能力从 Repository 中拆出:
|
|
9
|
+
* - refs 快照
|
|
10
|
+
* - v2 object-info
|
|
11
|
+
* - 协议能力广告
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* 创建基于 Smart HTTP 的远端查询对象
|
|
15
|
+
*
|
|
16
|
+
* 适用于只依赖远端 URL / 认证信息的查询,
|
|
17
|
+
* 例如 refs 快照和 object-info,不需要本地 repo 上下文。
|
|
18
|
+
*
|
|
19
|
+
* @param source - 远端来源
|
|
20
|
+
* @returns 远端查询对象
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { createHttpRemote } from "nano-git/remote/http";
|
|
25
|
+
*
|
|
26
|
+
* const remote = createHttpRemote({
|
|
27
|
+
* url: "https://github.com/user/repo.git",
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* const snapshot = await remote.readRefAdvertisement();
|
|
31
|
+
* const info = await remote.fetchObjectInfo([
|
|
32
|
+
* "95d09f2b10159347eece71399a7e2e907ea3df4f",
|
|
33
|
+
* ]);
|
|
34
|
+
*
|
|
35
|
+
* console.log(snapshot.defaultBranch);
|
|
36
|
+
* console.log(info.objects[0]?.size);
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
function createHttpRemote(source) {
|
|
40
|
+
const frozenSource = Object.freeze({
|
|
41
|
+
url: source.url,
|
|
42
|
+
token: source.token,
|
|
43
|
+
headers: source.headers ? Object.freeze({ ...source.headers }) : void 0
|
|
44
|
+
});
|
|
45
|
+
const transport = createV2HttpTransport(frozenSource.url, {
|
|
46
|
+
token: frozenSource.token,
|
|
47
|
+
headers: frozenSource.headers
|
|
48
|
+
});
|
|
49
|
+
return {
|
|
50
|
+
source: frozenSource,
|
|
51
|
+
advertise() {
|
|
52
|
+
return transport.advertise();
|
|
53
|
+
},
|
|
54
|
+
listRefs(options) {
|
|
55
|
+
return lsRefs(transport, options);
|
|
56
|
+
},
|
|
57
|
+
async readRefAdvertisement() {
|
|
58
|
+
return lsRefsToRefAdvertisement(await lsRefs(transport, {
|
|
59
|
+
symrefs: true,
|
|
60
|
+
peel: true,
|
|
61
|
+
refPrefixes: [
|
|
62
|
+
"HEAD",
|
|
63
|
+
"refs/heads/",
|
|
64
|
+
"refs/tags/"
|
|
65
|
+
]
|
|
66
|
+
}));
|
|
67
|
+
},
|
|
68
|
+
fetchObjectInfo(oids) {
|
|
69
|
+
return objectInfo(transport, oids);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { createHttpRemote };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/remote/types.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* 远端来源类型定义
|
|
4
|
+
*
|
|
5
|
+
* 只描述"从哪里读",不描述"写到哪里"。
|
|
6
|
+
* 可被远端查询 API 与仓库导入 API 共同复用。
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 远端 Git 数据来源
|
|
10
|
+
*/
|
|
11
|
+
interface RemoteSource {
|
|
12
|
+
/** 远端仓库 URL */
|
|
13
|
+
readonly url: string;
|
|
14
|
+
/** 认证 token(用于 bearer 或 basic auth) */
|
|
15
|
+
readonly token?: string;
|
|
16
|
+
/** 自定义请求头 */
|
|
17
|
+
readonly headers?: Record<string, string>;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { RemoteSource };
|
|
@@ -1,21 +1,8 @@
|
|
|
1
1
|
import { SHA1 } from "../../core/types.mjs";
|
|
2
|
+
import { RemoteSource } from "../../remote/types.mjs";
|
|
2
3
|
import { RefAdvertisement, RemoteRef } from "../../transport/protocol/types.mjs";
|
|
3
4
|
|
|
4
5
|
//#region src/repository/import/import-session-types.d.ts
|
|
5
|
-
/**
|
|
6
|
-
* 远端 Git 数据来源
|
|
7
|
-
*
|
|
8
|
-
* 只描述"从哪里读",不描述"写到哪里"。
|
|
9
|
-
* ImportSource 不包含命名空间映射规则。
|
|
10
|
-
*/
|
|
11
|
-
interface ImportSource {
|
|
12
|
-
/** 远端仓库 URL */
|
|
13
|
-
readonly url: string;
|
|
14
|
-
/** 认证 token(用于 bearer 或 basic auth) */
|
|
15
|
-
readonly token?: string;
|
|
16
|
-
/** 自定义请求头 */
|
|
17
|
-
readonly headers?: Record<string, string>;
|
|
18
|
-
}
|
|
19
6
|
/**
|
|
20
7
|
* 远端 ref 视图
|
|
21
8
|
*
|
|
@@ -244,7 +231,7 @@ interface ImportApplyResult {
|
|
|
244
231
|
* 想刷新远端状态时,必须重新创建 session。
|
|
245
232
|
*/
|
|
246
233
|
interface ImportSession {
|
|
247
|
-
readonly source:
|
|
234
|
+
readonly source: RemoteSource;
|
|
248
235
|
readonly advertisement: RefAdvertisement;
|
|
249
236
|
select(pattern: string): ImportView;
|
|
250
237
|
selectRefs(patterns: readonly string[]): ImportView;
|
|
@@ -274,7 +261,7 @@ interface RepoImportOperations {
|
|
|
274
261
|
* const branches = session.select("refs/heads/*");
|
|
275
262
|
* ```
|
|
276
263
|
*/
|
|
277
|
-
openImportSession(source:
|
|
264
|
+
openImportSession(source: RemoteSource): Promise<ImportSession>;
|
|
278
265
|
}
|
|
279
266
|
//#endregion
|
|
280
267
|
export { RepoImportOperations };
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { hashObject } from "../../core/hash-digest.mjs";
|
|
2
2
|
import { readObject, writeObject } from "../../objects/raw.mjs";
|
|
3
|
-
import { createV2HttpTransport } from "../../transport/client/upload-pack/http.mjs";
|
|
4
|
-
import { objectInfo } from "../../transport/client/upload-pack/object-info.mjs";
|
|
5
3
|
import { patchTree } from "../tree/tree-patch.mjs";
|
|
6
4
|
//#region src/repository/ops/object-operations.ts
|
|
7
5
|
/**
|
|
@@ -55,9 +53,6 @@ function createObjectRepositoryOperations(objects) {
|
|
|
55
53
|
},
|
|
56
54
|
patchTree(rootHash, ops) {
|
|
57
55
|
return patchTree(objects, rootHash, ops);
|
|
58
|
-
},
|
|
59
|
-
async fetchObjectInfo(url, oids, token) {
|
|
60
|
-
return objectInfo(createV2HttpTransport(url, { token }), oids);
|
|
61
56
|
}
|
|
62
57
|
};
|
|
63
58
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { GitAuthor, GitObject, SHA1, TreeEntry } from "../../core/types.mjs";
|
|
2
|
-
import { ObjectInfoQueryResult } from "../../transport/client/upload-pack/object-info.mjs";
|
|
3
2
|
import { TreePatchOp, TreePatchResult } from "../tree/tree-patch.mjs";
|
|
4
3
|
|
|
5
4
|
//#region src/repository/ops/object-types.d.ts
|
|
@@ -67,26 +66,6 @@ interface RepositoryObjectOperations {
|
|
|
67
66
|
* ```
|
|
68
67
|
*/
|
|
69
68
|
patchTree(rootHash: SHA1, ops: TreePatchOp[]): TreePatchResult;
|
|
70
|
-
/**
|
|
71
|
-
* 查询远端对象信息(协议 v2 object-info)
|
|
72
|
-
*
|
|
73
|
-
* 批量查询远端对象的元数据(如 size),无需下载对象内容。
|
|
74
|
-
* 仅在远端支持 Git Wire 协议 v2 时可用。
|
|
75
|
-
*
|
|
76
|
-
* @param url - 远端仓库 URL
|
|
77
|
-
* @param oids - 要查询的 OID 列表
|
|
78
|
-
* @param token - 可选认证 token
|
|
79
|
-
* @returns 对象信息列表(含 size 等元数据)
|
|
80
|
-
*
|
|
81
|
-
* @example
|
|
82
|
-
* ```ts
|
|
83
|
-
* const result = await repo.fetchObjectInfo("https://github.com/user/repo", [
|
|
84
|
-
* "95d09f2b10159347eece71399a7e2e907ea3df4f",
|
|
85
|
-
* ]);
|
|
86
|
-
* console.log(result.objects[0]?.size); // 文件大小
|
|
87
|
-
* ```
|
|
88
|
-
*/
|
|
89
|
-
fetchObjectInfo(url: string, oids: string[], token?: string): Promise<ObjectInfoQueryResult>;
|
|
90
69
|
}
|
|
91
70
|
/**
|
|
92
71
|
* 文件系统对象操作扩展
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { CreateSqliteRepositoryBackendOptions } from "../backend/sqlite.mjs";
|
|
2
|
+
import { Repository } from "./types.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/repository/sqlite.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* 创建基于 SQLite 文件的持久化仓库
|
|
7
|
+
*
|
|
8
|
+
* 相比直接使用 createSqliteRepositoryBackend + createRepository,
|
|
9
|
+
* 此函数提供了更简洁的一步到位接口。
|
|
10
|
+
*
|
|
11
|
+
* @param dbPath - SQLite 数据库文件路径
|
|
12
|
+
* @param options - 可选参数(如 walMode)
|
|
13
|
+
* @returns 仓库实例(附带 [Symbol.dispose](),可用 `using` 管理)
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* // 使用 using 自动释放(推荐)
|
|
18
|
+
* using repo = createSqliteRepository("/tmp/repo.sqlite");
|
|
19
|
+
* repo.createBranch("main", repo.createTree([]));
|
|
20
|
+
* repo.writeBlob(Buffer.from("data"));
|
|
21
|
+
* // 作用域结束时自动关闭数据库连接
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
declare function createSqliteRepository(dbPath: string, options?: CreateSqliteRepositoryBackendOptions): Repository & {
|
|
25
|
+
[Symbol.dispose](): void;
|
|
26
|
+
};
|
|
27
|
+
//#endregion
|
|
28
|
+
export { createSqliteRepository };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createSqliteRepositoryBackend } from "../backend/sqlite.mjs";
|
|
2
|
+
import { createRepository } from "./create.mjs";
|
|
3
|
+
//#region src/repository/sqlite.ts
|
|
4
|
+
/**
|
|
5
|
+
* SQLite 仓库便捷创建函数
|
|
6
|
+
*
|
|
7
|
+
* 一键创建基于 SQLite 的持久化 Git 仓库。
|
|
8
|
+
* 内部组合 createSqliteRepositoryBackend + createRepository,
|
|
9
|
+
* 返回的 repo 自带 [Symbol.dispose](),可用 `using` 管理生命周期。
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { createSqliteRepository } from "nano-git/repository/sqlite";
|
|
14
|
+
*
|
|
15
|
+
* using repo = createSqliteRepository("/tmp/cache.sqlite");
|
|
16
|
+
* const hash = repo.writeBlob(Buffer.from("hello world"));
|
|
17
|
+
* // 作用域结束时自动 db.close()
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* 创建基于 SQLite 文件的持久化仓库
|
|
22
|
+
*
|
|
23
|
+
* 相比直接使用 createSqliteRepositoryBackend + createRepository,
|
|
24
|
+
* 此函数提供了更简洁的一步到位接口。
|
|
25
|
+
*
|
|
26
|
+
* @param dbPath - SQLite 数据库文件路径
|
|
27
|
+
* @param options - 可选参数(如 walMode)
|
|
28
|
+
* @returns 仓库实例(附带 [Symbol.dispose](),可用 `using` 管理)
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* // 使用 using 自动释放(推荐)
|
|
33
|
+
* using repo = createSqliteRepository("/tmp/repo.sqlite");
|
|
34
|
+
* repo.createBranch("main", repo.createTree([]));
|
|
35
|
+
* repo.writeBlob(Buffer.from("data"));
|
|
36
|
+
* // 作用域结束时自动关闭数据库连接
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
function createSqliteRepository(dbPath, options) {
|
|
40
|
+
const backend = createSqliteRepositoryBackend(dbPath, options);
|
|
41
|
+
const repo = createRepository(backend);
|
|
42
|
+
return Object.assign(repo, { [Symbol.dispose]: () => backend[Symbol.dispose]() });
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
export { createSqliteRepository };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LsRefsEntry, ObjectInfoEntry, ObjectInfoResponse, V2CapabilityAdvertisement, V2CommandEntry, V2FetchRequest, V2FetchResponse, V2GitServiceTransport } from "./client/upload-pack/types.mjs";
|
|
2
|
+
import { LsRefsError, LsRefsOptions, lsRefs, lsRefsToRefAdvertisement, parseLsRefsResponse } from "./client/upload-pack/ls-refs.mjs";
|
|
2
3
|
import { ObjectInfoError, ObjectInfoQueryResult, ObjectInfoResult, objectInfo, parseObjectInfoResponse } from "./client/upload-pack/object-info.mjs";
|
|
3
4
|
import { V2CapabilityError, getCommandFeatures, hasCommand, parseV2CapabilityAdvertisement } from "./client/upload-pack/capability-advertisement.mjs";
|
|
4
5
|
import { V2SmartHttpError, createV2HttpTransport } from "./client/upload-pack/http.mjs";
|
|
5
|
-
import { LsRefsError, LsRefsOptions, lsRefs, lsRefsToRefAdvertisement, parseLsRefsResponse } from "./client/upload-pack/ls-refs.mjs";
|
|
6
6
|
import { V2FetchError, V2FetchParams, parseV2FetchResponse, v2Fetch, v2FetchObjects } from "./client/upload-pack/fetch.mjs";
|
|
7
7
|
export { type LsRefsEntry, LsRefsError, type LsRefsOptions, type ObjectInfoEntry, ObjectInfoError, type ObjectInfoQueryResult, type ObjectInfoResponse, type ObjectInfoResult, type V2CapabilityAdvertisement, V2CapabilityError, type V2CommandEntry, V2FetchError, type V2FetchParams, type V2FetchRequest, type V2FetchResponse, type V2GitServiceTransport, V2SmartHttpError, createV2HttpTransport, getCommandFeatures, hasCommand, lsRefs, lsRefsToRefAdvertisement, objectInfo, parseLsRefsResponse, parseObjectInfoResponse, parseV2CapabilityAdvertisement, parseV2FetchResponse, v2Fetch, v2FetchObjects };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { V2CapabilityError, getCommandFeatures, hasCommand, parseV2CapabilityAdvertisement } from "./client/upload-pack/capability-advertisement.mjs";
|
|
2
2
|
import { V2SmartHttpError, createV2HttpTransport } from "./client/upload-pack/http.mjs";
|
|
3
3
|
import { LsRefsError, lsRefs, lsRefsToRefAdvertisement, parseLsRefsResponse } from "./client/upload-pack/ls-refs.mjs";
|
|
4
|
-
import { V2FetchError, parseV2FetchResponse, v2Fetch, v2FetchObjects } from "./client/upload-pack/fetch.mjs";
|
|
5
4
|
import { ObjectInfoError, objectInfo, parseObjectInfoResponse } from "./client/upload-pack/object-info.mjs";
|
|
5
|
+
import { V2FetchError, parseV2FetchResponse, v2Fetch, v2FetchObjects } from "./client/upload-pack/fetch.mjs";
|
|
6
6
|
export { LsRefsError, ObjectInfoError, V2CapabilityError, V2FetchError, V2SmartHttpError, createV2HttpTransport, getCommandFeatures, hasCommand, lsRefs, lsRefsToRefAdvertisement, objectInfo, parseLsRefsResponse, parseObjectInfoResponse, parseV2CapabilityAdvertisement, parseV2FetchResponse, v2Fetch, v2FetchObjects };
|
|
@@ -0,0 +1,63 @@
|
|
|
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
|
+
const out = [];
|
|
26
|
+
for (const record of records) {
|
|
27
|
+
const mapped = mapInternalToVirtual(record);
|
|
28
|
+
if (mapped !== null) out.push(mapped);
|
|
29
|
+
}
|
|
30
|
+
return out;
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function mapInternalToVirtual(record) {
|
|
35
|
+
switch (record.op) {
|
|
36
|
+
case "add": return {
|
|
37
|
+
path: record.path,
|
|
38
|
+
type: "add"
|
|
39
|
+
};
|
|
40
|
+
case "modify": return {
|
|
41
|
+
path: record.path,
|
|
42
|
+
type: "modify"
|
|
43
|
+
};
|
|
44
|
+
case "delete": return {
|
|
45
|
+
path: record.path,
|
|
46
|
+
type: "delete"
|
|
47
|
+
};
|
|
48
|
+
case "rename": return {
|
|
49
|
+
path: record.to,
|
|
50
|
+
type: "rename",
|
|
51
|
+
oldPath: record.from
|
|
52
|
+
};
|
|
53
|
+
case "copy": return {
|
|
54
|
+
path: record.to,
|
|
55
|
+
type: "copy",
|
|
56
|
+
oldPath: record.from
|
|
57
|
+
};
|
|
58
|
+
case "revert": return null;
|
|
59
|
+
default: return record;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
export { createVirtualChangeLog };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { SHA1 } from "../core/types.mjs";
|
|
2
|
+
import { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError } from "../core/errors.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/workdir/core.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* 虚拟工作目录条目种类
|
|
7
|
+
*
|
|
8
|
+
* - `"blob"`: 普通文件或可执行文件(mode 100644 / 100755)
|
|
9
|
+
* - `"tree"`: 目录(mode 40000)
|
|
10
|
+
* - `"symlink"`: 符号链接(mode 120000)
|
|
11
|
+
*/
|
|
12
|
+
type VirtualEntryKind = "blob" | "tree" | "symlink";
|
|
13
|
+
/**
|
|
14
|
+
* 虚拟路径状态信息
|
|
15
|
+
*
|
|
16
|
+
* 由 `stat()` 返回,描述路径对应的节点属性。
|
|
17
|
+
*/
|
|
18
|
+
interface VirtualEntryStat {
|
|
19
|
+
/** 条目种类 */
|
|
20
|
+
readonly kind: VirtualEntryKind;
|
|
21
|
+
/** Git 文件模式(如 "100644"、"100755"、"40000"、"120000") */
|
|
22
|
+
readonly mode: string;
|
|
23
|
+
/** 文件大小(对 blob 和 symlink 有效;目录返回 0) */
|
|
24
|
+
readonly size: number;
|
|
25
|
+
/** 内容哈希(对 repo-backed 节点返回 origin 哈希;CoW 节点可能为 null) */
|
|
26
|
+
readonly hash: SHA1 | null;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 目录条目
|
|
30
|
+
*
|
|
31
|
+
* 由 `readdir()` 返回,描述目录下的子条目。
|
|
32
|
+
*/
|
|
33
|
+
interface VirtualDirEntry {
|
|
34
|
+
/** 条目名称(不含路径前缀) */
|
|
35
|
+
readonly name: string;
|
|
36
|
+
/** 条目种类 */
|
|
37
|
+
readonly kind: VirtualEntryKind;
|
|
38
|
+
/** Git 文件模式 */
|
|
39
|
+
readonly mode: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* 变更操作类型
|
|
43
|
+
*
|
|
44
|
+
* 用于 `listChanges()` 返回的变更记录。
|
|
45
|
+
*/
|
|
46
|
+
type VirtualChangeType = "add" | "modify" | "delete" | "rename" | "copy";
|
|
47
|
+
/**
|
|
48
|
+
* 单条变更记录
|
|
49
|
+
*
|
|
50
|
+
* 由 `listChanges()` 返回,描述 session 内的单次操作。
|
|
51
|
+
* 变更记录是会话内调试/测试辅助,不保证是最小 diff。
|
|
52
|
+
*/
|
|
53
|
+
interface VirtualChange {
|
|
54
|
+
/** 操作路径 */
|
|
55
|
+
readonly path: string;
|
|
56
|
+
/** 变更操作类型 */
|
|
57
|
+
readonly type: VirtualChangeType;
|
|
58
|
+
/** rename/copy 操作的源路径(其他操作为 undefined) */
|
|
59
|
+
readonly oldPath?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 创建 VirtualWorkdirSession 的选项
|
|
63
|
+
*/
|
|
64
|
+
interface CreateVirtualWorkdirSessionOptions {
|
|
65
|
+
/** 基线 tree 的 SHA-1 哈希 */
|
|
66
|
+
readonly baseTree: SHA1;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* VirtualWorkdirSession(虚拟工作目录会话)
|
|
70
|
+
*
|
|
71
|
+
* 提供独立生命周期的可变 tree 视图,基于 `baseTree + CoW overlay` 模型。
|
|
72
|
+
* 不绑定 commit,不涉及 Git index / 真实工作目录。
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* import { createMemoryRepository } from "nano-git/repository/memory";
|
|
77
|
+
* import { createVirtualWorkdirSession } from "nano-git/workdir/memory";
|
|
78
|
+
*
|
|
79
|
+
* const repo = createMemoryRepository();
|
|
80
|
+
* const tree = repo.writeTree(); // 初始空 tree
|
|
81
|
+
* const session = createVirtualWorkdirSession(repo, { baseTree: tree });
|
|
82
|
+
*
|
|
83
|
+
* session.writeFile("hello.txt", Buffer.from("world"));
|
|
84
|
+
* const newTree = session.writeTree();
|
|
85
|
+
* ```
|
|
86
|
+
*/
|
|
87
|
+
interface VirtualWorkdirSession {
|
|
88
|
+
/** 当前基线 tree 的 SHA-1 哈希 */
|
|
89
|
+
readonly baseTree: SHA1;
|
|
90
|
+
/** 路径是否存在 */
|
|
91
|
+
exists(path: string): boolean;
|
|
92
|
+
/** 获取路径状态信息,不存在时返回 null */
|
|
93
|
+
stat(path: string): VirtualEntryStat | null;
|
|
94
|
+
/** 读取目录内容,根目录为 "" */
|
|
95
|
+
readdir(path?: string): VirtualDirEntry[];
|
|
96
|
+
/** 读取文件内容 */
|
|
97
|
+
readFile(path: string): Buffer;
|
|
98
|
+
/** 读取符号链接目标 */
|
|
99
|
+
readLink(path: string): string;
|
|
100
|
+
/** 写入文件(新建或覆盖) */
|
|
101
|
+
writeFile(path: string, content: Buffer, options?: {
|
|
102
|
+
readonly mode?: "100644" | "100755";
|
|
103
|
+
}): void;
|
|
104
|
+
/** 写入符号链接(新建或覆盖) */
|
|
105
|
+
writeLink(path: string, target: string): void;
|
|
106
|
+
/** 创建目录(含必要父目录) */
|
|
107
|
+
mkdir(path: string): void;
|
|
108
|
+
/** 删除路径(文件、目录或符号链接) */
|
|
109
|
+
delete(path: string): void;
|
|
110
|
+
/**
|
|
111
|
+
* 重命名路径
|
|
112
|
+
*
|
|
113
|
+
* 只做路径重绑定,不退化为 delete + write。
|
|
114
|
+
* 目录重命名后,子项保持懒加载。
|
|
115
|
+
*/
|
|
116
|
+
rename(from: string, to: string): void;
|
|
117
|
+
/**
|
|
118
|
+
* 复制路径
|
|
119
|
+
*
|
|
120
|
+
* 新建 session node,共享 origin,不共享 node 身份。
|
|
121
|
+
* 目录复制为浅复制,子项保持懒加载。
|
|
122
|
+
*/
|
|
123
|
+
copy(from: string, to: string): void;
|
|
124
|
+
/**
|
|
125
|
+
* 恢复路径到其 origin
|
|
126
|
+
*
|
|
127
|
+
* 仅对当前 CoW 节点恢复其 origin(repo-backed 版本)。
|
|
128
|
+
* 对纯新建节点抛出 VirtualRevertNotSupportedError。
|
|
129
|
+
*/
|
|
130
|
+
revert(path: string): void;
|
|
131
|
+
/**
|
|
132
|
+
* 导出当前 overlay 为新 tree
|
|
133
|
+
*
|
|
134
|
+
* 只重新合成受影响目录,复用未修改节点的哈希。
|
|
135
|
+
* 不自动推进 baseTree。
|
|
136
|
+
*/
|
|
137
|
+
writeTree(): SHA1;
|
|
138
|
+
/**
|
|
139
|
+
* 重置 session 到指定基线 tree
|
|
140
|
+
*
|
|
141
|
+
* 丢弃全部 overlay 与变更历史。
|
|
142
|
+
*/
|
|
143
|
+
reset(baseTree: SHA1): void;
|
|
144
|
+
/**
|
|
145
|
+
* 列出会话内的变更记录
|
|
146
|
+
*
|
|
147
|
+
* 是会话内调试/测试辅助,不保证是最小 diff 引擎。
|
|
148
|
+
* 输出稳定、测试可断言即可。
|
|
149
|
+
*/
|
|
150
|
+
listChanges(): VirtualChange[];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* VirtualWorkdirBackend
|
|
154
|
+
*
|
|
155
|
+
* session 内部状态存储的抽象接口。
|
|
156
|
+
* memory / file / sqlite 后端通过实现此接口来提供不同的持久化策略。
|
|
157
|
+
*
|
|
158
|
+
* 本接口在后续 Phase 中会逐步补充完整方法签名。
|
|
159
|
+
* 当前仅为命名冻结与角色声明。
|
|
160
|
+
*/
|
|
161
|
+
interface VirtualWorkdirBackend {
|
|
162
|
+
/** 后端类型标识 */
|
|
163
|
+
readonly kind: "memory" | "file" | "sqlite";
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
export { CreateVirtualWorkdirSessionOptions, VirtualChange, VirtualChangeType, VirtualDirEntry, VirtualEntryKind, VirtualEntryStat, VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError, VirtualWorkdirBackend, VirtualWorkdirSession };
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError } from "../core/errors.mjs";
|
|
2
|
+
export { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualOriginUnavailableError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
//#region src/workdir/ids.ts
|
|
2
|
+
/** 根目录节点的固定 ID(每个 session 一致) */
|
|
3
|
+
const VIRTUAL_ROOT_NODE_ID = "root";
|
|
4
|
+
let nextNodeCounter = 1;
|
|
5
|
+
/**
|
|
6
|
+
* 分配新的 session 节点 ID
|
|
7
|
+
*/
|
|
8
|
+
function createNodeId() {
|
|
9
|
+
const id = `node:${nextNodeCounter}`;
|
|
10
|
+
nextNodeCounter += 1;
|
|
11
|
+
return id;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* 为仓库对象哈希派生稳定的 session 节点 ID(懒加载、未 copy 前复用)
|
|
15
|
+
*/
|
|
16
|
+
function originBackedNodeId(hash) {
|
|
17
|
+
return `origin:${hash}`;
|
|
18
|
+
}
|
|
19
|
+
//#endregion
|
|
20
|
+
export { VIRTUAL_ROOT_NODE_ID, createNodeId, originBackedNodeId };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { VIRTUAL_ROOT_NODE_ID } from "./ids.mjs";
|
|
2
|
+
import { createVirtualChangeLog } from "./change-log.mjs";
|
|
3
|
+
import { createRootDirectoryNode } from "./nodes.mjs";
|
|
4
|
+
//#region src/workdir/memory-backend.ts
|
|
5
|
+
/**
|
|
6
|
+
* Virtual Workdir 会话内存状态(与 ODB 后端无关的纯状态容器)
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 创建初始 session 状态(仅根目录节点,绑定 baseTree origin)
|
|
10
|
+
*/
|
|
11
|
+
function createVirtualWorkdirMemoryState(baseTree) {
|
|
12
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
13
|
+
nodes.set(VIRTUAL_ROOT_NODE_ID, createRootDirectoryNode(baseTree));
|
|
14
|
+
return {
|
|
15
|
+
baseTree,
|
|
16
|
+
nodes,
|
|
17
|
+
changeLog: createVirtualChangeLog()
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
//#endregion
|
|
21
|
+
export { createVirtualWorkdirMemoryState };
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { VIRTUAL_ROOT_NODE_ID } from "./ids.mjs";
|
|
2
|
+
import { cloneDirectoryOverlay, createEmptyDirectoryOverlay } from "./overlay.mjs";
|
|
3
|
+
//#region src/workdir/nodes.ts
|
|
4
|
+
/**
|
|
5
|
+
* Virtual Workdir 会话节点状态模型
|
|
6
|
+
*
|
|
7
|
+
* 节点身份(nodeId)与目录路径绑定分离;origin 描述 repo-backed 来源。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 创建绑定 repo 根 tree 的目录节点(带空 overlay)
|
|
11
|
+
*/
|
|
12
|
+
function createRootDirectoryNode(originTreeHash) {
|
|
13
|
+
return {
|
|
14
|
+
id: VIRTUAL_ROOT_NODE_ID,
|
|
15
|
+
origin: {
|
|
16
|
+
kind: "repo-tree",
|
|
17
|
+
hash: originTreeHash
|
|
18
|
+
},
|
|
19
|
+
state: {
|
|
20
|
+
kind: "directory",
|
|
21
|
+
overlay: createEmptyDirectoryOverlay()
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 节点是否携带可 revert 的 repo origin
|
|
27
|
+
*/
|
|
28
|
+
function nodeHasRepoOrigin(node) {
|
|
29
|
+
return node.origin.kind === "repo-tree" || node.origin.kind === "repo-blob";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* 目录 overlay 是否有 session 层修改
|
|
33
|
+
*/
|
|
34
|
+
function isDirectoryOverlayDirty(overlay) {
|
|
35
|
+
return overlay.addedEntries.size > 0 || overlay.deletedNames.size > 0;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* 将节点状态恢复到 origin 语义(丢弃 materialize / 目录 overlay)
|
|
39
|
+
*
|
|
40
|
+
* 无 repo origin 时返回原状态引用,由上层决定是否抛错。
|
|
41
|
+
*/
|
|
42
|
+
function revertNodeState(node) {
|
|
43
|
+
if (!nodeHasRepoOrigin(node)) return node;
|
|
44
|
+
if (node.state.kind === "directory") return {
|
|
45
|
+
...node,
|
|
46
|
+
state: {
|
|
47
|
+
kind: "directory",
|
|
48
|
+
overlay: createEmptyDirectoryOverlay()
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
if (node.state.kind === "file") {
|
|
52
|
+
const mode = node.state.mode;
|
|
53
|
+
return {
|
|
54
|
+
...node,
|
|
55
|
+
state: {
|
|
56
|
+
kind: "file",
|
|
57
|
+
mode
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
...node,
|
|
63
|
+
state: {
|
|
64
|
+
kind: "symlink",
|
|
65
|
+
mode: "120000"
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* 为 `copy` 创建新节点:共享 origin,目录为浅复制(子项绑定保留,但 nodeId 为新)
|
|
71
|
+
*/
|
|
72
|
+
function cloneSessionNodeForCopy(source, newId) {
|
|
73
|
+
const origin = source.origin;
|
|
74
|
+
if (source.state.kind === "directory") return {
|
|
75
|
+
id: newId,
|
|
76
|
+
origin,
|
|
77
|
+
state: {
|
|
78
|
+
kind: "directory",
|
|
79
|
+
overlay: cloneDirectoryOverlay(source.state.overlay)
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
if (source.state.kind === "file") {
|
|
83
|
+
const content = source.state.content;
|
|
84
|
+
return {
|
|
85
|
+
id: newId,
|
|
86
|
+
origin,
|
|
87
|
+
state: content === void 0 ? {
|
|
88
|
+
kind: "file",
|
|
89
|
+
mode: source.state.mode
|
|
90
|
+
} : {
|
|
91
|
+
kind: "file",
|
|
92
|
+
mode: source.state.mode,
|
|
93
|
+
content: Buffer.from(content)
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
const target = source.state.target;
|
|
98
|
+
return {
|
|
99
|
+
id: newId,
|
|
100
|
+
origin,
|
|
101
|
+
state: target === void 0 ? {
|
|
102
|
+
kind: "symlink",
|
|
103
|
+
mode: "120000"
|
|
104
|
+
} : {
|
|
105
|
+
kind: "symlink",
|
|
106
|
+
mode: "120000",
|
|
107
|
+
target: Buffer.from(target)
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { cloneSessionNodeForCopy, createRootDirectoryNode, isDirectoryOverlayDirty, revertNodeState };
|