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,274 @@
|
|
|
1
|
+
import { tryReadObject } from "../objects/raw.mjs";
|
|
2
|
+
//#region src/log/walk.ts
|
|
3
|
+
/**
|
|
4
|
+
* 提交日志遍历核心实现
|
|
5
|
+
*
|
|
6
|
+
* 提供 Generator 风格的 `walkLogEntries`,支持按提交时间降序
|
|
7
|
+
* 或严格拓扑序遍历提交历史。
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* // 从 HEAD 开始遍历最近 10 条提交
|
|
12
|
+
* const headHash = resolveRefHash(repo.refs, "HEAD")!;
|
|
13
|
+
* for (const entry of walkLogEntries(repo.objects, { from: [headHash], maxCount: 10 })) {
|
|
14
|
+
* console.log(entry.hash, entry.commit.message);
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* // 等价于 git log main..feature
|
|
18
|
+
* const featureHash = resolveRefHash(repo.refs, "refs/heads/feature")!;
|
|
19
|
+
* const mainHash = resolveRefHash(repo.refs, "refs/heads/main")!;
|
|
20
|
+
* for (const entry of walkLogEntries(repo.objects, { from: [featureHash], exclude: [mainHash] })) {
|
|
21
|
+
* console.log(entry.hash, entry.commit.message);
|
|
22
|
+
* }
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
var MaxHeapByTimestamp = class {
|
|
26
|
+
heap = [];
|
|
27
|
+
get size() {
|
|
28
|
+
return this.heap.length;
|
|
29
|
+
}
|
|
30
|
+
push(entry) {
|
|
31
|
+
this.heap.push(entry);
|
|
32
|
+
this.siftUp(this.heap.length - 1);
|
|
33
|
+
}
|
|
34
|
+
pop() {
|
|
35
|
+
if (this.heap.length === 0) return void 0;
|
|
36
|
+
const top = this.heap[0];
|
|
37
|
+
const bottom = this.heap.pop();
|
|
38
|
+
if (this.heap.length > 0) {
|
|
39
|
+
this.heap[0] = bottom;
|
|
40
|
+
this.siftDown(0);
|
|
41
|
+
}
|
|
42
|
+
return top;
|
|
43
|
+
}
|
|
44
|
+
siftUp(idx) {
|
|
45
|
+
const heap = this.heap;
|
|
46
|
+
while (idx > 0) {
|
|
47
|
+
const parent = idx - 1 >> 1;
|
|
48
|
+
if (heap[parent].commit.committer.timestamp >= heap[idx].commit.committer.timestamp) break;
|
|
49
|
+
[heap[parent], heap[idx]] = [heap[idx], heap[parent]];
|
|
50
|
+
idx = parent;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
siftDown(idx) {
|
|
54
|
+
const heap = this.heap;
|
|
55
|
+
const end = heap.length;
|
|
56
|
+
while (true) {
|
|
57
|
+
let largest = idx;
|
|
58
|
+
const left = (idx << 1) + 1;
|
|
59
|
+
const right = left + 1;
|
|
60
|
+
if (left < end && heap[left].commit.committer.timestamp > heap[largest].commit.committer.timestamp) largest = left;
|
|
61
|
+
if (right < end && heap[right].commit.committer.timestamp > heap[largest].commit.committer.timestamp) largest = right;
|
|
62
|
+
if (largest === idx) break;
|
|
63
|
+
[heap[idx], heap[largest]] = [heap[largest], heap[idx]];
|
|
64
|
+
idx = largest;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* 递归标记排除提交及其所有祖先
|
|
70
|
+
*/
|
|
71
|
+
function markExcluded(source, hash, excluded) {
|
|
72
|
+
const stack = [hash];
|
|
73
|
+
while (stack.length > 0) {
|
|
74
|
+
const current = stack.pop();
|
|
75
|
+
if (excluded.has(current)) continue;
|
|
76
|
+
excluded.add(current);
|
|
77
|
+
const obj = tryReadObject(source, current);
|
|
78
|
+
if (obj?.type === "commit") {
|
|
79
|
+
for (const parent of obj.parents) if (!excluded.has(parent)) stack.push(parent);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* 按提交时间降序遍历提交历史
|
|
85
|
+
*
|
|
86
|
+
* 使用最大堆按时间戳排序,每次弹出最新提交后将其 parent 入堆。
|
|
87
|
+
* 可配合 firstParent、since、until、skip、maxCount 等过滤条件。
|
|
88
|
+
*/
|
|
89
|
+
function* walkByDate(source, from, excluded, firstParent, skip, maxCount, since, until) {
|
|
90
|
+
const queue = new MaxHeapByTimestamp();
|
|
91
|
+
const seen = /* @__PURE__ */ new Set();
|
|
92
|
+
const visited = /* @__PURE__ */ new Set();
|
|
93
|
+
for (const hash of from) {
|
|
94
|
+
if (excluded.has(hash) || seen.has(hash)) continue;
|
|
95
|
+
const obj = tryReadObject(source, hash);
|
|
96
|
+
if (obj?.type !== "commit") continue;
|
|
97
|
+
seen.add(hash);
|
|
98
|
+
queue.push({
|
|
99
|
+
hash,
|
|
100
|
+
commit: obj
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
let skipped = 0;
|
|
104
|
+
let emitted = 0;
|
|
105
|
+
while (queue.size > 0) {
|
|
106
|
+
if (maxCount !== void 0 && emitted >= maxCount) break;
|
|
107
|
+
const entry = queue.pop();
|
|
108
|
+
if (visited.has(entry.hash)) continue;
|
|
109
|
+
if (excluded.has(entry.hash)) continue;
|
|
110
|
+
if (since !== void 0 && entry.commit.committer.timestamp < since) continue;
|
|
111
|
+
if (until !== void 0 && entry.commit.committer.timestamp > until) {
|
|
112
|
+
enqueueParents(source, entry.commit.parents, firstParent, excluded, seen, visited, queue);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (skipped < skip) {
|
|
116
|
+
skipped++;
|
|
117
|
+
visited.add(entry.hash);
|
|
118
|
+
enqueueParents(source, entry.commit.parents, firstParent, excluded, seen, visited, queue);
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
visited.add(entry.hash);
|
|
122
|
+
emitted++;
|
|
123
|
+
yield {
|
|
124
|
+
hash: entry.hash,
|
|
125
|
+
commit: entry.commit
|
|
126
|
+
};
|
|
127
|
+
enqueueParents(source, entry.commit.parents, firstParent, excluded, seen, visited, queue);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* 将提交的 parent 批量入堆
|
|
132
|
+
*/
|
|
133
|
+
function enqueueParents(source, parents, firstParent, excluded, seen, visited, queue) {
|
|
134
|
+
const targetParents = firstParent && parents.length > 0 ? [parents[0]] : parents;
|
|
135
|
+
for (const parentHash of targetParents) {
|
|
136
|
+
if (seen.has(parentHash) || excluded.has(parentHash)) continue;
|
|
137
|
+
seen.add(parentHash);
|
|
138
|
+
const obj = tryReadObject(source, parentHash);
|
|
139
|
+
if (obj?.type !== "commit") continue;
|
|
140
|
+
queue.push({
|
|
141
|
+
hash: parentHash,
|
|
142
|
+
commit: obj
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 生成严格拓扑序(子提交先于父提交)的提交迭代器
|
|
148
|
+
*
|
|
149
|
+
* 实现 Kahn 算法:
|
|
150
|
+
* 1. 收集所有起点可达的提交,构建 parent→children 映射
|
|
151
|
+
* 2. 统计每个提交的未输出子提交数(childCount)
|
|
152
|
+
* 3. 将 childCount === 0 的提交入堆(同层按时间戳降序)
|
|
153
|
+
* 4. 每次弹出堆顶输出,递减其 parent 的 childCount
|
|
154
|
+
* 5. parent 的 childCount 归零时入堆
|
|
155
|
+
*/
|
|
156
|
+
function* walkTopo(source, from, excluded, firstParent, skip, maxCount, since, until) {
|
|
157
|
+
const commits = /* @__PURE__ */ new Map();
|
|
158
|
+
const children = /* @__PURE__ */ new Map();
|
|
159
|
+
const stack = [...from];
|
|
160
|
+
while (stack.length > 0) {
|
|
161
|
+
const hash = stack.pop();
|
|
162
|
+
if (commits.has(hash) || excluded.has(hash)) continue;
|
|
163
|
+
const obj = tryReadObject(source, hash);
|
|
164
|
+
if (obj?.type !== "commit") continue;
|
|
165
|
+
commits.set(hash, obj);
|
|
166
|
+
if (!children.has(hash)) children.set(hash, []);
|
|
167
|
+
const targetParents = firstParent && obj.parents.length > 0 ? [obj.parents[0]] : obj.parents;
|
|
168
|
+
for (const parentHash of targetParents) {
|
|
169
|
+
if (commits.has(parentHash) || excluded.has(parentHash)) continue;
|
|
170
|
+
if (!children.has(parentHash)) children.set(parentHash, []);
|
|
171
|
+
children.get(parentHash).push(hash);
|
|
172
|
+
stack.push(parentHash);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (commits.size === 0) return;
|
|
176
|
+
const childCount = /* @__PURE__ */ new Map();
|
|
177
|
+
const pq = new MaxHeapByTimestamp();
|
|
178
|
+
for (const hash of commits.keys()) {
|
|
179
|
+
const count = children.get(hash)?.length ?? 0;
|
|
180
|
+
childCount.set(hash, count);
|
|
181
|
+
if (count === 0) pq.push({
|
|
182
|
+
hash,
|
|
183
|
+
commit: commits.get(hash)
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
let skipped = 0;
|
|
187
|
+
let emitted = 0;
|
|
188
|
+
while (pq.size > 0) {
|
|
189
|
+
if (maxCount !== void 0 && emitted >= maxCount) break;
|
|
190
|
+
const entry = pq.pop();
|
|
191
|
+
if (since !== void 0 && entry.commit.committer.timestamp < since) {
|
|
192
|
+
decrementParentCounts(entry.commit.parents, firstParent, childCount, commits, pq);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (until !== void 0 && entry.commit.committer.timestamp > until) {
|
|
196
|
+
decrementParentCounts(entry.commit.parents, firstParent, childCount, commits, pq);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (skipped < skip) {
|
|
200
|
+
skipped++;
|
|
201
|
+
decrementParentCounts(entry.commit.parents, firstParent, childCount, commits, pq);
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
emitted++;
|
|
205
|
+
yield {
|
|
206
|
+
hash: entry.hash,
|
|
207
|
+
commit: entry.commit
|
|
208
|
+
};
|
|
209
|
+
decrementParentCounts(entry.commit.parents, firstParent, childCount, commits, pq);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* 递减 parent 的 childCount,归零时入堆
|
|
214
|
+
*/
|
|
215
|
+
function decrementParentCounts(parents, firstParent, childCount, commits, pq) {
|
|
216
|
+
const targetParents = firstParent && parents.length > 0 ? [parents[0]] : parents;
|
|
217
|
+
for (const parentHash of targetParents) {
|
|
218
|
+
const count = childCount.get(parentHash);
|
|
219
|
+
if (count === void 0) continue;
|
|
220
|
+
const newCount = count - 1;
|
|
221
|
+
childCount.set(parentHash, newCount);
|
|
222
|
+
if (newCount === 0) {
|
|
223
|
+
const parentCommit = commits.get(parentHash);
|
|
224
|
+
if (parentCommit) pq.push({
|
|
225
|
+
hash: parentHash,
|
|
226
|
+
commit: parentCommit
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* 遍历提交历史日志
|
|
233
|
+
*
|
|
234
|
+
* 从指定的起点哈希出发,沿 parent 链回溯,按指定排序策略输出提交。
|
|
235
|
+
* 不涉及 ref 解析——调用方需自行将 ref 名称解析为 SHA1。
|
|
236
|
+
*
|
|
237
|
+
* @param source - 对象源(通常是 `Repository.objects`)
|
|
238
|
+
* @param options - 遍历选项
|
|
239
|
+
* @returns 按序输出的提交日志条目生成器
|
|
240
|
+
*
|
|
241
|
+
* @example
|
|
242
|
+
* ```ts
|
|
243
|
+
* const headHash = resolveRefHash(repo.refs, "HEAD")!;
|
|
244
|
+
* for (const entry of walkLogEntries(repo.objects, { from: [headHash], maxCount: 5 })) {
|
|
245
|
+
* console.log(entry.hash, entry.commit.subject);
|
|
246
|
+
* }
|
|
247
|
+
* ```
|
|
248
|
+
*
|
|
249
|
+
* @example
|
|
250
|
+
* ```ts
|
|
251
|
+
* // 等价于 git log --oneline --since=1600000000 --until=1700000000
|
|
252
|
+
* for (const entry of walkLogEntries(repo.objects, {
|
|
253
|
+
* from: [headHash],
|
|
254
|
+
* since: 1600000000,
|
|
255
|
+
* until: 1700000000,
|
|
256
|
+
* })) {
|
|
257
|
+
* console.log(entry.hash.slice(0, 7), entry.commit.message.split("\n")[0]);
|
|
258
|
+
* }
|
|
259
|
+
* ```
|
|
260
|
+
*/
|
|
261
|
+
function walkLogEntries(source, options = {}) {
|
|
262
|
+
const { from = [], exclude = [], skip: skipCount = 0, order = "date", since, until, firstParent = false, maxCount } = options;
|
|
263
|
+
if (from.length === 0) return emptyGenerator();
|
|
264
|
+
const excluded = /* @__PURE__ */ new Set();
|
|
265
|
+
for (const hash of exclude) markExcluded(source, hash, excluded);
|
|
266
|
+
if (order === "topo") return walkTopo(source, from, excluded, firstParent, skipCount, maxCount, since, until);
|
|
267
|
+
return walkByDate(source, from, excluded, firstParent, skipCount, maxCount, since, until);
|
|
268
|
+
}
|
|
269
|
+
/** 返回一个空的 Generator(不产生任何值,直接 done) */
|
|
270
|
+
function emptyGenerator() {
|
|
271
|
+
return [][Symbol.iterator]();
|
|
272
|
+
}
|
|
273
|
+
//#endregion
|
|
274
|
+
export { walkLogEntries };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ObjectDatabase } from "../core/types/odb.mjs";
|
|
2
|
+
import { Database } from "bun:sqlite";
|
|
3
|
+
|
|
4
|
+
//#region src/odb/sqlite.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* 创建基于 SQLite 的对象数据库
|
|
7
|
+
*
|
|
8
|
+
* @param db - 已打开的 bun:sqlite Database 实例
|
|
9
|
+
* @returns 符合 ObjectDatabase 接口的存储后端
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { Database } from "bun:sqlite";
|
|
14
|
+
* const db = new Database("/tmp/repo.sqlite");
|
|
15
|
+
* const store = createSqliteObjectStore(db);
|
|
16
|
+
*
|
|
17
|
+
* store.ingest(raw);
|
|
18
|
+
* const obj = store.read(hash);
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
declare function createSqliteObjectStore(db: Database): ObjectDatabase;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { createSqliteObjectStore };
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { ObjectNotFoundError } from "../core/errors.mjs";
|
|
2
|
+
import { sha1 } from "../core/types.mjs";
|
|
3
|
+
import { hashObject } from "../core/hash-digest.mjs";
|
|
4
|
+
//#region src/odb/sqlite.ts
|
|
5
|
+
/**
|
|
6
|
+
* 基于 SQLite 的对象数据库(raw-first)
|
|
7
|
+
*
|
|
8
|
+
* 所有对象存储在 SQLite 数据库的 objects 表中。
|
|
9
|
+
* 使用 INSERT OR IGNORE 实现幂等写入,使用 db.transaction() 实现批量原子写入。
|
|
10
|
+
*
|
|
11
|
+
* 表创建由上层 createSqliteRepositoryBackend 负责,
|
|
12
|
+
* 本模块只操作已存在的表,不负责 DDL。
|
|
13
|
+
*/
|
|
14
|
+
/**
|
|
15
|
+
* 创建基于 SQLite 的对象数据库
|
|
16
|
+
*
|
|
17
|
+
* @param db - 已打开的 bun:sqlite Database 实例
|
|
18
|
+
* @returns 符合 ObjectDatabase 接口的存储后端
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* import { Database } from "bun:sqlite";
|
|
23
|
+
* const db = new Database("/tmp/repo.sqlite");
|
|
24
|
+
* const store = createSqliteObjectStore(db);
|
|
25
|
+
*
|
|
26
|
+
* store.ingest(raw);
|
|
27
|
+
* const obj = store.read(hash);
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
function createSqliteObjectStore(db) {
|
|
31
|
+
const selectStmt = db.query("SELECT hash, type, content FROM objects WHERE hash = ?");
|
|
32
|
+
const existsStmt = db.query("SELECT 1 FROM objects WHERE hash = ?");
|
|
33
|
+
const insertStmt = db.query("INSERT OR IGNORE INTO objects (hash, type, content) VALUES (?, ?, ?)");
|
|
34
|
+
const deleteStmt = db.query("DELETE FROM objects WHERE hash = ?");
|
|
35
|
+
const listStmt = db.query("SELECT hash FROM objects ORDER BY hash");
|
|
36
|
+
/** 批量插入的事务包装 */
|
|
37
|
+
const ingestManyTx = db.transaction((objects) => {
|
|
38
|
+
for (const raw of objects) {
|
|
39
|
+
const expectedHash = hashObject(raw.type, raw.content);
|
|
40
|
+
if (expectedHash !== raw.hash) throw new Error(`RawGitObject hash mismatch: expected ${expectedHash}, got ${raw.hash}`);
|
|
41
|
+
insertStmt.run(raw.hash, raw.type, raw.content);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
return {
|
|
45
|
+
ingest(raw) {
|
|
46
|
+
const expectedHash = hashObject(raw.type, raw.content);
|
|
47
|
+
if (expectedHash !== raw.hash) throw new Error(`RawGitObject hash mismatch: expected ${expectedHash}, got ${raw.hash}`);
|
|
48
|
+
insertStmt.run(raw.hash, raw.type, raw.content);
|
|
49
|
+
},
|
|
50
|
+
ingestMany(objects) {
|
|
51
|
+
ingestManyTx(objects);
|
|
52
|
+
},
|
|
53
|
+
read(hash) {
|
|
54
|
+
const row = selectStmt.get(hash);
|
|
55
|
+
if (!row) throw new ObjectNotFoundError(hash);
|
|
56
|
+
return {
|
|
57
|
+
hash: sha1(row.hash),
|
|
58
|
+
type: row.type,
|
|
59
|
+
content: Buffer.from(row.content)
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
tryRead(hash) {
|
|
63
|
+
const row = selectStmt.get(hash);
|
|
64
|
+
if (!row) return;
|
|
65
|
+
return {
|
|
66
|
+
hash: sha1(row.hash),
|
|
67
|
+
type: row.type,
|
|
68
|
+
content: Buffer.from(row.content)
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
exists(hash) {
|
|
72
|
+
return existsStmt.get(hash) !== null;
|
|
73
|
+
},
|
|
74
|
+
list() {
|
|
75
|
+
return listStmt.all().map((row) => sha1(row.hash));
|
|
76
|
+
},
|
|
77
|
+
delete(hash) {
|
|
78
|
+
deleteStmt.run(hash);
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
export { createSqliteObjectStore };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ShallowStore } from "../../core/types/shallow.mjs";
|
|
2
|
+
import { Database } from "bun:sqlite";
|
|
3
|
+
|
|
4
|
+
//#region src/refs/shallow/sqlite.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* 创建基于 SQLite 的 shallow 边界存储
|
|
7
|
+
*
|
|
8
|
+
* @param db - 已打开的 bun:sqlite Database 实例
|
|
9
|
+
* @returns 符合 ShallowStore 接口的存储后端
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { Database } from "bun:sqlite";
|
|
14
|
+
* const db = new Database("/tmp/repo.sqlite");
|
|
15
|
+
* const store = createSqliteShallowStore(db);
|
|
16
|
+
*
|
|
17
|
+
* store.write([hashA, hashB]);
|
|
18
|
+
* console.log(store.isShallow(hashA)); // true
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
declare function createSqliteShallowStore(db: Database): ShallowStore;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { createSqliteShallowStore };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { sha1 } from "../../core/types.mjs";
|
|
2
|
+
//#region src/refs/shallow/sqlite.ts
|
|
3
|
+
/**
|
|
4
|
+
* 基于 SQLite 的 Shallow 存储
|
|
5
|
+
*
|
|
6
|
+
* 所有 shallow 边界存储在 SQLite 数据库的 shallow 表中。
|
|
7
|
+
* write 使用 DELETE + INSERT 全量替换模式(与 memory/file 后端一致)。
|
|
8
|
+
* applyUpdate 使用 SQL 事务做增量更新。
|
|
9
|
+
*
|
|
10
|
+
* 表创建由上层 createSqliteRepositoryBackend 负责,
|
|
11
|
+
* 本模块只操作已存在的表,不负责 DDL。
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* 创建基于 SQLite 的 shallow 边界存储
|
|
15
|
+
*
|
|
16
|
+
* @param db - 已打开的 bun:sqlite Database 实例
|
|
17
|
+
* @returns 符合 ShallowStore 接口的存储后端
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { Database } from "bun:sqlite";
|
|
22
|
+
* const db = new Database("/tmp/repo.sqlite");
|
|
23
|
+
* const store = createSqliteShallowStore(db);
|
|
24
|
+
*
|
|
25
|
+
* store.write([hashA, hashB]);
|
|
26
|
+
* console.log(store.isShallow(hashA)); // true
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
function createSqliteShallowStore(db) {
|
|
30
|
+
const selectAllStmt = db.query("SELECT hash FROM shallow ORDER BY hash");
|
|
31
|
+
const selectExistsStmt = db.query("SELECT 1 FROM shallow WHERE hash = ?");
|
|
32
|
+
const deleteAllStmt = db.query("DELETE FROM shallow");
|
|
33
|
+
const insertStmt = db.query("INSERT OR IGNORE INTO shallow (hash) VALUES (?)");
|
|
34
|
+
const deleteOneStmt = db.query("DELETE FROM shallow WHERE hash = ?");
|
|
35
|
+
/** 全量替换事务 */
|
|
36
|
+
const replaceAllTx = db.transaction((boundaries) => {
|
|
37
|
+
deleteAllStmt.run();
|
|
38
|
+
for (const hash of boundaries) insertStmt.run(hash);
|
|
39
|
+
});
|
|
40
|
+
/** 增量更新事务 */
|
|
41
|
+
const applyUpdateTx = db.transaction((update) => {
|
|
42
|
+
for (const hash of update.unshallow) deleteOneStmt.run(hash);
|
|
43
|
+
for (const hash of update.shallow) insertStmt.run(hash);
|
|
44
|
+
});
|
|
45
|
+
return {
|
|
46
|
+
read() {
|
|
47
|
+
return selectAllStmt.all().map((row) => sha1(row.hash));
|
|
48
|
+
},
|
|
49
|
+
write(boundaries) {
|
|
50
|
+
replaceAllTx(boundaries);
|
|
51
|
+
},
|
|
52
|
+
applyUpdate(update) {
|
|
53
|
+
applyUpdateTx(update);
|
|
54
|
+
},
|
|
55
|
+
isShallow(hash) {
|
|
56
|
+
return selectExistsStmt.get(hash) !== null;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { createSqliteShallowStore };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { RefStore } from "../core/types/refs.mjs";
|
|
2
|
+
import { Database } from "bun:sqlite";
|
|
3
|
+
|
|
4
|
+
//#region src/refs/sqlite.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* 创建基于 SQLite 的 RefStore
|
|
7
|
+
*
|
|
8
|
+
* @param db - 已打开的 bun:sqlite Database 实例(生命周期由调用方管理)
|
|
9
|
+
* @returns 符合 RefStore 接口的存储后端(含事务支持)
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* import { Database } from "bun:sqlite";
|
|
14
|
+
* const db = new Database("/tmp/repo.sqlite");
|
|
15
|
+
* const store = createSqliteRefStore(db);
|
|
16
|
+
*
|
|
17
|
+
* store.write("refs/heads/main", "abc123");
|
|
18
|
+
* const content = store.read("refs/heads/main");
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
declare function createSqliteRefStore(db: Database): RefStore;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { createSqliteRefStore };
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { RefNotFoundError, TransactionError } from "../core/errors.mjs";
|
|
2
|
+
import { validateRefName, validateRefPrefix } from "./names.mjs";
|
|
3
|
+
//#region src/refs/sqlite.ts
|
|
4
|
+
/**
|
|
5
|
+
* 基于 SQLite 的 Refs 存储
|
|
6
|
+
*
|
|
7
|
+
* 所有引用存储在 SQLite 数据库的 refs 表中。
|
|
8
|
+
* 使用 INSERT OR REPLACE 实现幂等写入,使用 db.transaction() 实现事务原子性。
|
|
9
|
+
*
|
|
10
|
+
* 表创建由上层 createSqliteRepositoryBackend 负责,
|
|
11
|
+
* 本模块只操作已存在的表,不负责 DDL。
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* 创建基于 SQLite 的 RefStore
|
|
15
|
+
*
|
|
16
|
+
* @param db - 已打开的 bun:sqlite Database 实例(生命周期由调用方管理)
|
|
17
|
+
* @returns 符合 RefStore 接口的存储后端(含事务支持)
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```ts
|
|
21
|
+
* import { Database } from "bun:sqlite";
|
|
22
|
+
* const db = new Database("/tmp/repo.sqlite");
|
|
23
|
+
* const store = createSqliteRefStore(db);
|
|
24
|
+
*
|
|
25
|
+
* store.write("refs/heads/main", "abc123");
|
|
26
|
+
* const content = store.read("refs/heads/main");
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
function createSqliteRefStore(db) {
|
|
30
|
+
const selectStmt = db.query("SELECT target FROM refs WHERE name = ?");
|
|
31
|
+
const selectExistsStmt = db.query("SELECT 1 FROM refs WHERE name = ?");
|
|
32
|
+
const insertStmt = db.query("INSERT OR REPLACE INTO refs (name, target) VALUES (?, ?)");
|
|
33
|
+
const deleteStmt = db.query("DELETE FROM refs WHERE name = ?");
|
|
34
|
+
const listPrefixStmt = db.query("SELECT name FROM refs WHERE name >= ? AND name < ? ORDER BY name");
|
|
35
|
+
const listAllStmt = db.query("SELECT name FROM refs WHERE name LIKE 'refs/%' ORDER BY name");
|
|
36
|
+
/**
|
|
37
|
+
* 开启一个新的事务
|
|
38
|
+
*
|
|
39
|
+
* 所有变更暂存于 JS Map,commit() 时通过 SQLite 事务原子性写入。
|
|
40
|
+
*/
|
|
41
|
+
function beginTransaction(hooks) {
|
|
42
|
+
const pending = /* @__PURE__ */ new Map();
|
|
43
|
+
const snapshot = /* @__PURE__ */ new Map();
|
|
44
|
+
for (const row of listAllStmt.all()) {
|
|
45
|
+
const val = selectStmt.get(row.name);
|
|
46
|
+
if (val !== null) snapshot.set(row.name, val.target);
|
|
47
|
+
}
|
|
48
|
+
let committed = false;
|
|
49
|
+
return {
|
|
50
|
+
get pendingCount() {
|
|
51
|
+
return pending.size;
|
|
52
|
+
},
|
|
53
|
+
write(ref, content) {
|
|
54
|
+
if (committed) throw new TransactionError("Transaction already committed");
|
|
55
|
+
validateRefName(ref);
|
|
56
|
+
pending.set(ref, content.trimEnd());
|
|
57
|
+
},
|
|
58
|
+
delete(ref) {
|
|
59
|
+
if (committed) throw new TransactionError("Transaction already committed");
|
|
60
|
+
validateRefName(ref);
|
|
61
|
+
const inDb = selectExistsStmt.get(ref) !== null;
|
|
62
|
+
const inPending = pending.has(ref);
|
|
63
|
+
if (!inDb && !inPending) throw new RefNotFoundError(ref);
|
|
64
|
+
pending.set(ref, null);
|
|
65
|
+
},
|
|
66
|
+
commit() {
|
|
67
|
+
if (committed) throw new TransactionError("Transaction already committed");
|
|
68
|
+
committed = true;
|
|
69
|
+
const txSnapshot = freezePending(pending);
|
|
70
|
+
try {
|
|
71
|
+
for (const hook of hooks ?? []) hook.onPrepare?.(txSnapshot);
|
|
72
|
+
db.transaction(() => {
|
|
73
|
+
for (const [ref, content] of pending) if (content === null) {
|
|
74
|
+
if (!snapshot.has(ref) && !selectExistsStmt.get(ref)) throw new RefNotFoundError(ref);
|
|
75
|
+
deleteStmt.run(ref);
|
|
76
|
+
} else insertStmt.run(ref, content);
|
|
77
|
+
})();
|
|
78
|
+
for (const hook of hooks ?? []) hook.onCommitted?.(txSnapshot);
|
|
79
|
+
} catch (e) {
|
|
80
|
+
for (const hook of hooks ?? []) hook.onAborted?.(txSnapshot);
|
|
81
|
+
throw e;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
rollback() {
|
|
85
|
+
if (committed) return;
|
|
86
|
+
committed = true;
|
|
87
|
+
const txSnapshot = freezePending(pending);
|
|
88
|
+
for (const hook of hooks ?? []) hook.onAborted?.(txSnapshot);
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return {
|
|
93
|
+
read(ref) {
|
|
94
|
+
validateRefName(ref);
|
|
95
|
+
return selectStmt.get(ref)?.target ?? null;
|
|
96
|
+
},
|
|
97
|
+
write(ref, content) {
|
|
98
|
+
validateRefName(ref);
|
|
99
|
+
insertStmt.run(ref, content.trimEnd());
|
|
100
|
+
},
|
|
101
|
+
delete(ref) {
|
|
102
|
+
validateRefName(ref);
|
|
103
|
+
if (!selectExistsStmt.get(ref)) throw new RefNotFoundError(ref);
|
|
104
|
+
deleteStmt.run(ref);
|
|
105
|
+
},
|
|
106
|
+
list(prefix) {
|
|
107
|
+
validateRefPrefix(prefix);
|
|
108
|
+
const end = prefix + "";
|
|
109
|
+
return listPrefixStmt.all(prefix, end).map((row) => row.name);
|
|
110
|
+
},
|
|
111
|
+
listAll() {
|
|
112
|
+
return listAllStmt.all().map((row) => row.name);
|
|
113
|
+
},
|
|
114
|
+
beginTransaction
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* 将 pending Map 冻结为只读快照
|
|
119
|
+
*/
|
|
120
|
+
function freezePending(pending) {
|
|
121
|
+
const writes = [];
|
|
122
|
+
const deletes = [];
|
|
123
|
+
for (const [ref, content] of pending) if (content === null) deletes.push({ ref });
|
|
124
|
+
else writes.push({
|
|
125
|
+
ref,
|
|
126
|
+
content
|
|
127
|
+
});
|
|
128
|
+
return Object.freeze({
|
|
129
|
+
pendingCount: pending.size,
|
|
130
|
+
writes: Object.freeze(writes),
|
|
131
|
+
deletes: Object.freeze(deletes)
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
//#endregion
|
|
135
|
+
export { createSqliteRefStore };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { RemoteSource } from "./types.mjs";
|
|
2
|
+
import { RefAdvertisement } from "../transport/protocol/types.mjs";
|
|
3
|
+
import { LsRefsEntry, V2CapabilityAdvertisement } from "../transport/client/upload-pack/types.mjs";
|
|
4
|
+
import { LsRefsOptions } from "../transport/client/upload-pack/ls-refs.mjs";
|
|
5
|
+
import { ObjectInfoQueryResult } from "../transport/client/upload-pack/object-info.mjs";
|
|
6
|
+
|
|
7
|
+
//#region src/remote/http.d.ts
|
|
8
|
+
/**
|
|
9
|
+
* 远端 HTTP 查询接口
|
|
10
|
+
*/
|
|
11
|
+
interface HttpRemote {
|
|
12
|
+
/** 远端来源配置 */
|
|
13
|
+
readonly source: Readonly<RemoteSource>;
|
|
14
|
+
/** 读取 v2 能力广告 */
|
|
15
|
+
advertise(): Promise<V2CapabilityAdvertisement>;
|
|
16
|
+
/** 原样执行 ls-refs 查询 */
|
|
17
|
+
listRefs(options?: LsRefsOptions): Promise<LsRefsEntry[]>;
|
|
18
|
+
/** 读取适合高层 API 使用的 ref 快照 */
|
|
19
|
+
readRefAdvertisement(): Promise<RefAdvertisement>;
|
|
20
|
+
/** 查询对象元数据(协议 v2 object-info) */
|
|
21
|
+
fetchObjectInfo(oids: string[]): Promise<ObjectInfoQueryResult>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* 创建基于 Smart HTTP 的远端查询对象
|
|
25
|
+
*
|
|
26
|
+
* 适用于只依赖远端 URL / 认证信息的查询,
|
|
27
|
+
* 例如 refs 快照和 object-info,不需要本地 repo 上下文。
|
|
28
|
+
*
|
|
29
|
+
* @param source - 远端来源
|
|
30
|
+
* @returns 远端查询对象
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import { createHttpRemote } from "nano-git/remote/http";
|
|
35
|
+
*
|
|
36
|
+
* const remote = createHttpRemote({
|
|
37
|
+
* url: "https://github.com/user/repo.git",
|
|
38
|
+
* });
|
|
39
|
+
*
|
|
40
|
+
* const snapshot = await remote.readRefAdvertisement();
|
|
41
|
+
* const info = await remote.fetchObjectInfo([
|
|
42
|
+
* "95d09f2b10159347eece71399a7e2e907ea3df4f",
|
|
43
|
+
* ]);
|
|
44
|
+
*
|
|
45
|
+
* console.log(snapshot.defaultBranch);
|
|
46
|
+
* console.log(info.objects[0]?.size);
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
declare function createHttpRemote(source: RemoteSource): HttpRemote;
|
|
50
|
+
//#endregion
|
|
51
|
+
export { HttpRemote, createHttpRemote };
|