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,55 @@
|
|
|
1
|
+
import { VirtualOriginUnavailableError } from "../core/errors.mjs";
|
|
2
|
+
import { tryReadObject } from "../objects/raw.mjs";
|
|
3
|
+
//#region src/workdir/origin.ts
|
|
4
|
+
/**
|
|
5
|
+
* 从对象源读取 repo-backed origin(tree/blob)
|
|
6
|
+
*
|
|
7
|
+
* 仅依赖 ObjectSource,不绑定具体 ODB 后端实现。
|
|
8
|
+
*/
|
|
9
|
+
/**
|
|
10
|
+
* 读取 tree 对象;缺失时抛 VirtualOriginUnavailableError
|
|
11
|
+
*/
|
|
12
|
+
function readRepoTree(source, hash, path) {
|
|
13
|
+
const obj = tryReadObject(source, hash);
|
|
14
|
+
if (obj === void 0) throw new VirtualOriginUnavailableError(path, `Origin tree object missing: ${hash}`);
|
|
15
|
+
if (obj.type !== "tree") throw new VirtualOriginUnavailableError(path, `Expected tree at origin, got ${obj.type}`);
|
|
16
|
+
return obj;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 读取 blob 原始内容;缺失时抛 VirtualOriginUnavailableError
|
|
20
|
+
*/
|
|
21
|
+
function readRepoBlobContent(source, hash, path) {
|
|
22
|
+
const obj = tryReadObject(source, hash);
|
|
23
|
+
if (obj === void 0) throw new VirtualOriginUnavailableError(path, `Origin blob object missing: ${hash}`);
|
|
24
|
+
if (obj.type !== "blob") throw new VirtualOriginUnavailableError(path, `Expected blob at origin, got ${obj.type}`);
|
|
25
|
+
return obj.content;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 根据 tree 条目构造节点 origin
|
|
29
|
+
*/
|
|
30
|
+
function treeEntryToNodeOrigin(entry) {
|
|
31
|
+
if (entry.mode === "40000") return {
|
|
32
|
+
kind: "repo-tree",
|
|
33
|
+
hash: entry.hash
|
|
34
|
+
};
|
|
35
|
+
if (entry.mode === "100644" || entry.mode === "100755" || entry.mode === "120000") return {
|
|
36
|
+
kind: "repo-blob",
|
|
37
|
+
mode: entry.mode,
|
|
38
|
+
hash: entry.hash
|
|
39
|
+
};
|
|
40
|
+
return {
|
|
41
|
+
kind: "repo-blob",
|
|
42
|
+
mode: "100644",
|
|
43
|
+
hash: entry.hash
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Git mode 转为 VirtualEntryKind
|
|
48
|
+
*/
|
|
49
|
+
function modeToVirtualEntryKind(mode) {
|
|
50
|
+
if (mode === "40000") return "tree";
|
|
51
|
+
if (mode === "120000") return "symlink";
|
|
52
|
+
return "blob";
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
export { modeToVirtualEntryKind, readRepoBlobContent, readRepoTree, treeEntryToNodeOrigin };
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
//#region src/workdir/overlay.ts
|
|
2
|
+
/**
|
|
3
|
+
* 创建空目录 overlay
|
|
4
|
+
*/
|
|
5
|
+
function createEmptyDirectoryOverlay() {
|
|
6
|
+
return {
|
|
7
|
+
addedEntries: /* @__PURE__ */ new Map(),
|
|
8
|
+
deletedNames: /* @__PURE__ */ new Set()
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 克隆目录 overlay(深拷贝 Map/Set)
|
|
13
|
+
*/
|
|
14
|
+
function cloneDirectoryOverlay(overlay) {
|
|
15
|
+
return {
|
|
16
|
+
addedEntries: new Map(overlay.addedEntries),
|
|
17
|
+
deletedNames: new Set(overlay.deletedNames)
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 按 RFC 规则合成目录子项列表
|
|
22
|
+
*
|
|
23
|
+
* 1. 从 origin 去掉 deletedNames
|
|
24
|
+
* 2. addedEntries 覆盖同名 origin
|
|
25
|
+
* 3. 仅存在于 addedEntries 的条目追加
|
|
26
|
+
* 4. 按 Git tree 名称排序
|
|
27
|
+
*/
|
|
28
|
+
function mergeDirectoryChildren(originChildren, overlay, addedEntryModes) {
|
|
29
|
+
const byName = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const child of originChildren) {
|
|
31
|
+
if (overlay.deletedNames.has(child.name)) continue;
|
|
32
|
+
const overrideId = overlay.addedEntries.get(child.name);
|
|
33
|
+
if (overrideId !== void 0) {
|
|
34
|
+
const mode = addedEntryModes.get(child.name) ?? child.mode;
|
|
35
|
+
byName.set(child.name, {
|
|
36
|
+
name: child.name,
|
|
37
|
+
mode,
|
|
38
|
+
nodeId: overrideId
|
|
39
|
+
});
|
|
40
|
+
} else byName.set(child.name, {
|
|
41
|
+
name: child.name,
|
|
42
|
+
mode: child.mode,
|
|
43
|
+
nodeId: child.nodeId
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
for (const [name, nodeId] of overlay.addedEntries) {
|
|
47
|
+
if (byName.has(name)) continue;
|
|
48
|
+
const mode = addedEntryModes.get(name);
|
|
49
|
+
if (mode === void 0) throw new Error(`Missing mode for added directory entry '${name}'`);
|
|
50
|
+
byName.set(name, {
|
|
51
|
+
name,
|
|
52
|
+
mode,
|
|
53
|
+
nodeId
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const merged = Array.from(byName.values());
|
|
57
|
+
merged.sort((a, b) => a.name.localeCompare(b.name));
|
|
58
|
+
return merged;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* 在目录 overlay 中绑定或覆盖条目(create / modify / rename 目标 / copy 目标)
|
|
62
|
+
*/
|
|
63
|
+
function overlayBindEntry(overlay, name, nodeId) {
|
|
64
|
+
const addedEntries = new Map(overlay.addedEntries);
|
|
65
|
+
const deletedNames = new Set(overlay.deletedNames);
|
|
66
|
+
addedEntries.set(name, nodeId);
|
|
67
|
+
deletedNames.delete(name);
|
|
68
|
+
return {
|
|
69
|
+
addedEntries,
|
|
70
|
+
deletedNames
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* 在目录 overlay 中删除条目(tombstone;不递归清理子节点存储)
|
|
75
|
+
*/
|
|
76
|
+
function overlayTombstoneEntry(overlay, name) {
|
|
77
|
+
const addedEntries = new Map(overlay.addedEntries);
|
|
78
|
+
const deletedNames = new Set(overlay.deletedNames);
|
|
79
|
+
if (addedEntries.has(name)) addedEntries.delete(name);
|
|
80
|
+
else deletedNames.add(name);
|
|
81
|
+
return {
|
|
82
|
+
addedEntries,
|
|
83
|
+
deletedNames
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* 在同一目录内重命名:复用 nodeId,仅改绑定名
|
|
88
|
+
*/
|
|
89
|
+
function overlayRenameEntry(overlay, fromName, toName, nodeId) {
|
|
90
|
+
let next = overlayTombstoneEntry(overlay, fromName);
|
|
91
|
+
next = overlayBindEntry(next, toName, nodeId);
|
|
92
|
+
return next;
|
|
93
|
+
}
|
|
94
|
+
//#endregion
|
|
95
|
+
export { cloneDirectoryOverlay, createEmptyDirectoryOverlay, mergeDirectoryChildren, overlayBindEntry, overlayRenameEntry, overlayTombstoneEntry };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 规范化 readdir 等 API 的目录路径参数
|
|
3
|
+
*
|
|
4
|
+
* `undefined` 与 `""` 均视为根目录。
|
|
5
|
+
*/
|
|
6
|
+
function normalizeDirectoryPath(path) {
|
|
7
|
+
if (path === void 0 || path === "") return "";
|
|
8
|
+
assertValidVirtualPath(path);
|
|
9
|
+
return path;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* 校验非根虚拟路径格式(用于文件/子路径操作)
|
|
13
|
+
*
|
|
14
|
+
* @throws 路径为空或格式非法时抛出 Error
|
|
15
|
+
*/
|
|
16
|
+
function assertValidVirtualPath(path) {
|
|
17
|
+
if (path === "") throw new Error("Path must not be empty");
|
|
18
|
+
validateVirtualPathSegments(path);
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 校验路径段规则(根路径 `""` 由调用方单独处理)
|
|
22
|
+
*/
|
|
23
|
+
function validateVirtualPathSegments(path) {
|
|
24
|
+
if (path.startsWith("/")) throw new Error(`Path must not start with '/': ${path}`);
|
|
25
|
+
if (path.endsWith("/")) throw new Error(`Path must not end with '/': ${path}`);
|
|
26
|
+
if (path.includes("//")) throw new Error(`Path must not contain consecutive slashes: ${path}`);
|
|
27
|
+
for (const segment of path.split("/")) {
|
|
28
|
+
if (segment === "." || segment === "..") throw new Error(`Path must not contain '.' or '..': ${path}`);
|
|
29
|
+
if (segment === "") throw new Error(`Path must not contain empty segments: ${path}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 将路径拆分为段(不含空段)
|
|
34
|
+
*
|
|
35
|
+
* 调用前须保证路径已通过 `assertValidVirtualPath`。
|
|
36
|
+
*/
|
|
37
|
+
function splitPathSegments(path) {
|
|
38
|
+
return path.split("/");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* 返回父路径;单段路径返回 `null`(表示无父目录,父为根)
|
|
42
|
+
*/
|
|
43
|
+
function parentPath(path) {
|
|
44
|
+
assertValidVirtualPath(path);
|
|
45
|
+
const lastSlash = path.lastIndexOf("/");
|
|
46
|
+
if (lastSlash === -1) return null;
|
|
47
|
+
return path.slice(0, lastSlash);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* 返回路径最后一段(条目名)
|
|
51
|
+
*/
|
|
52
|
+
function baseName(path) {
|
|
53
|
+
assertValidVirtualPath(path);
|
|
54
|
+
const lastSlash = path.lastIndexOf("/");
|
|
55
|
+
return lastSlash === -1 ? path : path.slice(lastSlash + 1);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* 组合父路径与条目名
|
|
59
|
+
*
|
|
60
|
+
* @param parent - `null` 表示根下直接子项
|
|
61
|
+
*/
|
|
62
|
+
function joinPath(parent, name) {
|
|
63
|
+
if (name === "" || name.includes("/")) throw new Error(`Invalid entry name: ${name}`);
|
|
64
|
+
if (parent === null) return name;
|
|
65
|
+
if (parent === "") return name;
|
|
66
|
+
assertValidVirtualPath(parent);
|
|
67
|
+
return `${parent}/${name}`;
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { assertValidVirtualPath, baseName, joinPath, normalizeDirectoryPath, parentPath, splitPathSegments };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { VirtualNotDirectoryError } from "../core/errors.mjs";
|
|
2
|
+
import { VIRTUAL_ROOT_NODE_ID, originBackedNodeId } from "./ids.mjs";
|
|
3
|
+
import { mergeDirectoryChildren } from "./overlay.mjs";
|
|
4
|
+
import { readRepoTree, treeEntryToNodeOrigin } from "./origin.mjs";
|
|
5
|
+
import { assertValidVirtualPath, joinPath, splitPathSegments } from "./path.mjs";
|
|
6
|
+
//#region src/workdir/session-internal.ts
|
|
7
|
+
/**
|
|
8
|
+
* Virtual Workdir session 内部共享逻辑
|
|
9
|
+
*
|
|
10
|
+
* 供 session.ts 与 write-tree.ts 复用:
|
|
11
|
+
* - 路径解析(resolvePath)
|
|
12
|
+
* - 目录子项展开(listDirectoryChildren)
|
|
13
|
+
* - origin 节点懒注册(ensureNodeFromTreeEntry)
|
|
14
|
+
*/
|
|
15
|
+
function getRootNode(state) {
|
|
16
|
+
const root = state.nodes.get(VIRTUAL_ROOT_NODE_ID);
|
|
17
|
+
if (root === void 0) throw new Error("Virtual workdir session is missing root node");
|
|
18
|
+
return root;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 从根目录沿路径分段解析到目标节点
|
|
22
|
+
*/
|
|
23
|
+
function resolvePath(source, state, path) {
|
|
24
|
+
assertValidVirtualPath(path);
|
|
25
|
+
const segments = splitPathSegments(path);
|
|
26
|
+
let current = getRootNode(state);
|
|
27
|
+
let currentPath = "";
|
|
28
|
+
for (const segment of segments) {
|
|
29
|
+
if (current.state.kind !== "directory") return {
|
|
30
|
+
found: false,
|
|
31
|
+
node: null
|
|
32
|
+
};
|
|
33
|
+
const child = listDirectoryChildren(source, state, current, currentPath).find((c) => c.name === segment);
|
|
34
|
+
if (child === void 0) return {
|
|
35
|
+
found: false,
|
|
36
|
+
node: null
|
|
37
|
+
};
|
|
38
|
+
currentPath = joinPath(currentPath === "" ? null : currentPath, segment);
|
|
39
|
+
const childNode = state.nodes.get(child.nodeId);
|
|
40
|
+
if (childNode === void 0) return {
|
|
41
|
+
found: false,
|
|
42
|
+
node: null
|
|
43
|
+
};
|
|
44
|
+
current = childNode;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
found: true,
|
|
48
|
+
node: current
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 根据父节点与子条目名解析子节点
|
|
53
|
+
*/
|
|
54
|
+
function resolveChild(source, state, parentNode, parentPath, name) {
|
|
55
|
+
if (parentNode.state.kind !== "directory") return {
|
|
56
|
+
found: false,
|
|
57
|
+
node: null
|
|
58
|
+
};
|
|
59
|
+
const child = listDirectoryChildren(source, state, parentNode, parentPath).find((c) => c.name === name);
|
|
60
|
+
if (child === void 0) return {
|
|
61
|
+
found: false,
|
|
62
|
+
node: null
|
|
63
|
+
};
|
|
64
|
+
const childNode = state.nodes.get(child.nodeId);
|
|
65
|
+
if (childNode === void 0) return {
|
|
66
|
+
found: false,
|
|
67
|
+
node: null
|
|
68
|
+
};
|
|
69
|
+
return {
|
|
70
|
+
found: true,
|
|
71
|
+
node: childNode
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* 确保 origin 条目对应的 session 节点已懒注册
|
|
76
|
+
*/
|
|
77
|
+
function ensureNodeFromTreeEntry(state, entry) {
|
|
78
|
+
const id = originBackedNodeId(entry.hash);
|
|
79
|
+
if (!state.nodes.has(id)) {
|
|
80
|
+
const origin = treeEntryToNodeOrigin(entry);
|
|
81
|
+
let nodeState;
|
|
82
|
+
if (entry.mode === "40000") nodeState = {
|
|
83
|
+
kind: "directory",
|
|
84
|
+
overlay: {
|
|
85
|
+
addedEntries: /* @__PURE__ */ new Map(),
|
|
86
|
+
deletedNames: /* @__PURE__ */ new Set()
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
else if (entry.mode === "120000") nodeState = {
|
|
90
|
+
kind: "symlink",
|
|
91
|
+
mode: "120000"
|
|
92
|
+
};
|
|
93
|
+
else nodeState = {
|
|
94
|
+
kind: "file",
|
|
95
|
+
mode: entry.mode === "100755" ? "100755" : "100644"
|
|
96
|
+
};
|
|
97
|
+
state.nodes.set(id, {
|
|
98
|
+
id,
|
|
99
|
+
origin,
|
|
100
|
+
state: nodeState
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return id;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 展开目录的完整子项列表(origin + overlay 合成)
|
|
107
|
+
*/
|
|
108
|
+
function listDirectoryChildren(source, state, dirNode, dirPath) {
|
|
109
|
+
if (dirNode.state.kind !== "directory") throw new VirtualNotDirectoryError(dirPath);
|
|
110
|
+
let originChildren = [];
|
|
111
|
+
if (dirNode.origin.kind === "repo-tree") originChildren = readRepoTree(source, dirNode.origin.hash, dirPath).entries.map((entry) => {
|
|
112
|
+
const childNodeId = ensureNodeFromTreeEntry(state, entry);
|
|
113
|
+
return {
|
|
114
|
+
name: entry.name,
|
|
115
|
+
mode: entry.mode,
|
|
116
|
+
nodeId: childNodeId
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
const addedModes = buildAddedModes(state, dirNode);
|
|
120
|
+
return mergeDirectoryChildren(originChildren, dirNode.state.overlay, addedModes);
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 构建 overlay addedEntries 对应的 mode map
|
|
124
|
+
*/
|
|
125
|
+
function buildAddedModes(state, dirNode) {
|
|
126
|
+
if (dirNode.state.kind !== "directory") return /* @__PURE__ */ new Map();
|
|
127
|
+
const addedModes = /* @__PURE__ */ new Map();
|
|
128
|
+
for (const name of dirNode.state.overlay.addedEntries.keys()) {
|
|
129
|
+
const nodeId = dirNode.state.overlay.addedEntries.get(name);
|
|
130
|
+
const node = state.nodes.get(nodeId);
|
|
131
|
+
if (node !== void 0) addedModes.set(name, sessionNodeMode(node));
|
|
132
|
+
}
|
|
133
|
+
return addedModes;
|
|
134
|
+
}
|
|
135
|
+
function sessionNodeMode(node) {
|
|
136
|
+
if (node.state.kind === "directory") return "40000";
|
|
137
|
+
if (node.state.kind === "symlink") return "120000";
|
|
138
|
+
return node.state.mode;
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
export { listDirectoryChildren, resolveChild, resolvePath };
|
|
@@ -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 };
|