nano-git 0.3.0 → 0.3.2
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 +6 -0
- package/dist/workdir/change-index-plan.mjs +76 -0
- package/dist/workdir/change-index.d.mts +21 -0
- package/dist/workdir/change-index.mjs +413 -0
- package/dist/workdir/core.d.mts +64 -63
- package/dist/workdir/directory-view.mjs +197 -0
- package/dist/workdir/dirty-dir-plan.mjs +73 -0
- package/dist/workdir/dirty-dir.d.mts +28 -0
- package/dist/workdir/dirty-dir.mjs +32 -0
- package/dist/workdir/file-backend.d.mts +34 -12
- package/dist/workdir/file-backend.mjs +161 -94
- package/dist/workdir/file.d.mts +2 -2
- package/dist/workdir/file.mjs +2 -2
- package/dist/workdir/ids.d.mts +1 -1
- package/dist/workdir/ids.mjs +3 -3
- package/dist/workdir/memory-backend.mjs +34 -50
- package/dist/workdir/memory.d.mts +2 -3
- package/dist/workdir/memory.mjs +2 -3
- package/dist/workdir/nodes.d.mts +7 -7
- package/dist/workdir/nodes.mjs +3 -9
- package/dist/workdir/overlay.d.mts +3 -3
- package/dist/workdir/sqlite-backend.d.mts +31 -14
- package/dist/workdir/sqlite-backend.mjs +238 -142
- package/dist/workdir/sqlite.d.mts +2 -2
- package/dist/workdir/sqlite.mjs +2 -2
- package/dist/workdir/state-store.d.mts +23 -12
- package/dist/workdir/workdir-path.mjs +348 -0
- package/dist/workdir/workdir-transaction.mjs +115 -0
- package/dist/workdir/workdir.d.mts +30 -0
- package/dist/workdir/workdir.mjs +280 -0
- package/dist/workdir/write-tree.mjs +108 -44
- package/package.json +2 -1
- package/dist/workdir/change-log.d.mts +0 -25
- package/dist/workdir/change-log.mjs +0 -69
- package/dist/workdir/memory-backend.d.mts +0 -17
- package/dist/workdir/session-id.mjs +0 -18
- package/dist/workdir/session-internal.mjs +0 -141
- package/dist/workdir/session.d.mts +0 -30
- package/dist/workdir/session.mjs +0 -407
|
@@ -1,30 +1,47 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ObjectDatabase } from "../core/types/odb.mjs";
|
|
2
|
+
import { CreateVirtualWorkdirOptions, VirtualWorkdir } from "./core.mjs";
|
|
2
3
|
import { Database } from "bun:sqlite";
|
|
3
4
|
|
|
4
5
|
//#region src/workdir/sqlite-backend.d.ts
|
|
5
|
-
/**
|
|
6
|
-
interface
|
|
6
|
+
/** SQLite 连接层的可选参数 */
|
|
7
|
+
interface SqliteVirtualWorkdirConnectionOptions {
|
|
7
8
|
/** 开启 WAL 模式,默认 true */
|
|
8
9
|
readonly walMode?: boolean;
|
|
9
10
|
}
|
|
11
|
+
/** 打开 SQLite VirtualWorkdir 的可选参数 */
|
|
12
|
+
interface OpenSqliteVirtualWorkdirOptions extends CreateVirtualWorkdirOptions, SqliteVirtualWorkdirConnectionOptions {
|
|
13
|
+
/** 不存在时按 baseTree 初始化 */
|
|
14
|
+
readonly create?: boolean;
|
|
15
|
+
}
|
|
10
16
|
/**
|
|
11
|
-
* SQLite
|
|
17
|
+
* 基于 SQLite 的 VirtualWorkdir
|
|
18
|
+
*
|
|
19
|
+
* 返回值附带 `[Symbol.dispose]()`,用于释放内部数据库连接。
|
|
12
20
|
*/
|
|
13
|
-
|
|
14
|
-
/** 释放 SQLite 数据库连接 */
|
|
21
|
+
type SqliteVirtualWorkdir = VirtualWorkdir & {
|
|
15
22
|
[Symbol.dispose](): void;
|
|
16
|
-
}
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* 打开基于 SQLite 的持久化 VirtualWorkdir
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* using workdir = openSqliteVirtualWorkdir(repo.objects, ":memory:", "demo", {
|
|
30
|
+
* baseTree: tree,
|
|
31
|
+
* create: true,
|
|
32
|
+
* });
|
|
33
|
+
* expect(workdir.baseTree).toBe(tree);
|
|
34
|
+
* ```
|
|
35
|
+
*/
|
|
36
|
+
declare function openSqliteVirtualWorkdir(source: ObjectDatabase, dbPath: string, workdirKey: string, options: OpenSqliteVirtualWorkdirOptions): SqliteVirtualWorkdir;
|
|
17
37
|
/**
|
|
18
|
-
*
|
|
38
|
+
* 删除指定 key 上的 SQLite VirtualWorkdir
|
|
19
39
|
*
|
|
20
40
|
* @example
|
|
21
41
|
* ```ts
|
|
22
|
-
*
|
|
23
|
-
* const sessionId = backend.createSession({ baseTree: tree });
|
|
24
|
-
* const session = backend.openSession(repo.objects, sessionId);
|
|
25
|
-
* expect(session.baseTree).toBe(tree);
|
|
42
|
+
* deleteSqliteVirtualWorkdir("/tmp/workdir.sqlite", "demo");
|
|
26
43
|
* ```
|
|
27
44
|
*/
|
|
28
|
-
declare function
|
|
45
|
+
declare function deleteSqliteVirtualWorkdir(dbPath: string, workdirKey: string, options?: SqliteVirtualWorkdirConnectionOptions): void;
|
|
29
46
|
//#endregion
|
|
30
|
-
export {
|
|
47
|
+
export { OpenSqliteVirtualWorkdirOptions, SqliteVirtualWorkdir, deleteSqliteVirtualWorkdir, openSqliteVirtualWorkdir };
|
|
@@ -1,91 +1,85 @@
|
|
|
1
1
|
import { sha1 } from "../core/types.mjs";
|
|
2
2
|
import { createRootDirectoryNode } from "./nodes.mjs";
|
|
3
|
-
import {
|
|
4
|
-
import { openVirtualWorkdirSession } from "./session.mjs";
|
|
3
|
+
import { openVirtualWorkdir } from "./workdir.mjs";
|
|
5
4
|
import { Database } from "bun:sqlite";
|
|
6
5
|
//#region src/workdir/sqlite-backend.ts
|
|
7
6
|
/**
|
|
8
7
|
* Virtual Workdir SQLite backend
|
|
9
8
|
*/
|
|
10
|
-
const WORKDIR_SQLITE_SCHEMA_VERSION =
|
|
11
|
-
const WORKDIR_CHANGES_SESSION_ORDER_INDEX = "workdir_changes_session_id_id_idx";
|
|
9
|
+
const WORKDIR_SQLITE_SCHEMA_VERSION = 6;
|
|
12
10
|
/**
|
|
13
|
-
*
|
|
11
|
+
* 打开基于 SQLite 的持久化 VirtualWorkdir
|
|
14
12
|
*
|
|
15
13
|
* @example
|
|
16
14
|
* ```ts
|
|
17
|
-
* using
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
15
|
+
* using workdir = openSqliteVirtualWorkdir(repo.objects, ":memory:", "demo", {
|
|
16
|
+
* baseTree: tree,
|
|
17
|
+
* create: true,
|
|
18
|
+
* });
|
|
19
|
+
* expect(workdir.baseTree).toBe(tree);
|
|
21
20
|
* ```
|
|
22
21
|
*/
|
|
23
|
-
function
|
|
22
|
+
function openSqliteVirtualWorkdir(source, dbPath, workdirKey, options) {
|
|
24
23
|
const db = new Database(dbPath);
|
|
25
|
-
let disposed = false;
|
|
26
24
|
if (options.walMode !== false) db.run("PRAGMA journal_mode = WAL");
|
|
27
25
|
ensureSchema(db);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
assertBackendAvailable(disposed);
|
|
32
|
-
const sessionId = createVirtualWorkdirSessionId();
|
|
33
|
-
createSqliteVirtualWorkdirStateStore(db, sessionId).reset(options.baseTree);
|
|
34
|
-
return sessionId;
|
|
35
|
-
},
|
|
36
|
-
openSession(source, sessionId) {
|
|
37
|
-
assertBackendAvailable(disposed);
|
|
38
|
-
if (!hasSession(db, sessionId)) throw new Error(`Virtual workdir session not found: ${sessionId}`);
|
|
39
|
-
validateSessionIntegrity(db, sessionId);
|
|
40
|
-
return openVirtualWorkdirSession(source, createSqliteVirtualWorkdirStateStore(db, sessionId));
|
|
41
|
-
},
|
|
42
|
-
deleteSession(sessionId) {
|
|
43
|
-
assertBackendAvailable(disposed);
|
|
44
|
-
if (!hasSession(db, sessionId)) throw new Error(`Virtual workdir session not found: ${sessionId}`);
|
|
45
|
-
deleteSessionRows(db, sessionId);
|
|
46
|
-
},
|
|
47
|
-
listSessions() {
|
|
48
|
-
assertBackendAvailable(disposed);
|
|
49
|
-
return db.query("SELECT session_id FROM workdir_sessions ORDER BY session_id").all().map((row) => row.session_id).filter((sessionId) => {
|
|
50
|
-
try {
|
|
51
|
-
validateSessionIntegrity(db, sessionId);
|
|
52
|
-
return true;
|
|
53
|
-
} catch {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
},
|
|
58
|
-
[Symbol.dispose]() {
|
|
59
|
-
if (disposed) return;
|
|
60
|
-
disposed = true;
|
|
26
|
+
const store = createSqliteVirtualWorkdirStateStore(db, workdirKey);
|
|
27
|
+
if (!hasWorkdir(db, workdirKey)) {
|
|
28
|
+
if (options.create !== true) {
|
|
61
29
|
db.close();
|
|
30
|
+
throw new Error(`Virtual workdir not found: ${workdirKey}`);
|
|
62
31
|
}
|
|
63
|
-
|
|
32
|
+
store.reset(options.baseTree);
|
|
33
|
+
}
|
|
34
|
+
validateWorkdirIntegrity(db, workdirKey);
|
|
35
|
+
let disposed = false;
|
|
36
|
+
const workdir = openVirtualWorkdir(source, store);
|
|
37
|
+
return Object.assign(workdir, { [Symbol.dispose]() {
|
|
38
|
+
if (disposed) return;
|
|
39
|
+
disposed = true;
|
|
40
|
+
db.close();
|
|
41
|
+
} });
|
|
64
42
|
}
|
|
65
|
-
|
|
66
|
-
|
|
43
|
+
/**
|
|
44
|
+
* 删除指定 key 上的 SQLite VirtualWorkdir
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* deleteSqliteVirtualWorkdir("/tmp/workdir.sqlite", "demo");
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
function deleteSqliteVirtualWorkdir(dbPath, workdirKey, options = {}) {
|
|
52
|
+
const db = new Database(dbPath);
|
|
53
|
+
try {
|
|
54
|
+
if (options.walMode !== false) db.run("PRAGMA journal_mode = WAL");
|
|
55
|
+
ensureSchema(db);
|
|
56
|
+
if (!hasWorkdir(db, workdirKey)) throw new Error(`Virtual workdir not found: ${workdirKey}`);
|
|
57
|
+
deleteWorkdirRows(db, workdirKey);
|
|
58
|
+
} finally {
|
|
59
|
+
db.close();
|
|
60
|
+
}
|
|
67
61
|
}
|
|
68
62
|
/**
|
|
69
|
-
* 创建单个
|
|
63
|
+
* 创建单个 SQLite VirtualWorkdir 的状态存储
|
|
70
64
|
*
|
|
71
65
|
* @example
|
|
72
66
|
* ```ts
|
|
73
|
-
* const store = createSqliteVirtualWorkdirStateStore(db,
|
|
67
|
+
* const store = createSqliteVirtualWorkdirStateStore(db, "demo");
|
|
74
68
|
* expect(store.kind).toBe("sqlite");
|
|
75
69
|
* ```
|
|
76
70
|
*/
|
|
77
|
-
function createSqliteVirtualWorkdirStateStore(db,
|
|
71
|
+
function createSqliteVirtualWorkdirStateStore(db, workdirKey) {
|
|
78
72
|
const transactImpl = db.transaction((fn) => fn());
|
|
79
|
-
const readBaseTreeStmt = db.query("SELECT base_tree FROM
|
|
80
|
-
const
|
|
73
|
+
const readBaseTreeStmt = db.query("SELECT base_tree FROM workdirs WHERE workdir_key = ?");
|
|
74
|
+
const upsertWorkdirStmt = db.query("INSERT INTO workdirs (workdir_key, base_tree) VALUES (?, ?) ON CONFLICT(workdir_key) DO UPDATE SET base_tree = excluded.base_tree");
|
|
81
75
|
const getNodeStmt = db.query(`SELECT node_id, origin_kind, origin_hash, origin_mode, state_kind, state_mode, content, target, directory_overlay
|
|
82
76
|
FROM workdir_nodes
|
|
83
|
-
WHERE
|
|
77
|
+
WHERE workdir_key = ? AND node_id = ?`);
|
|
84
78
|
const setNodeStmt = db.query(`INSERT INTO workdir_nodes (
|
|
85
|
-
|
|
79
|
+
workdir_key, node_id, origin_kind, origin_hash, origin_mode,
|
|
86
80
|
state_kind, state_mode, content, target, directory_overlay
|
|
87
81
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
88
|
-
ON CONFLICT(
|
|
82
|
+
ON CONFLICT(workdir_key, node_id) DO UPDATE SET
|
|
89
83
|
origin_kind = excluded.origin_kind,
|
|
90
84
|
origin_hash = excluded.origin_hash,
|
|
91
85
|
origin_mode = excluded.origin_mode,
|
|
@@ -94,16 +88,57 @@ function createSqliteVirtualWorkdirStateStore(db, sessionId) {
|
|
|
94
88
|
content = excluded.content,
|
|
95
89
|
target = excluded.target,
|
|
96
90
|
directory_overlay = excluded.directory_overlay`);
|
|
97
|
-
const deleteNodeStmt = db.query("DELETE FROM workdir_nodes WHERE
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
91
|
+
const deleteNodeStmt = db.query("DELETE FROM workdir_nodes WHERE workdir_key = ? AND node_id = ?");
|
|
92
|
+
const clearNodesStmt = db.query("DELETE FROM workdir_nodes WHERE workdir_key = ?");
|
|
93
|
+
const listChangesStmt = db.query(`SELECT path, previous_kind, previous_mode, previous_hash, current_kind, current_mode, current_hash, source_kind, source_path
|
|
94
|
+
FROM workdir_changes
|
|
95
|
+
WHERE workdir_key = ?
|
|
96
|
+
ORDER BY path`);
|
|
97
|
+
const getChangeStmt = db.query(`SELECT path, previous_kind, previous_mode, previous_hash, current_kind, current_mode, current_hash, source_kind, source_path
|
|
98
|
+
FROM workdir_changes
|
|
99
|
+
WHERE workdir_key = ? AND path = ?`);
|
|
100
|
+
const upsertChangeStmt = db.query(`INSERT INTO workdir_changes (
|
|
101
|
+
workdir_key, path, previous_kind, previous_mode, previous_hash,
|
|
102
|
+
current_kind, current_mode, current_hash, source_kind, source_path
|
|
103
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
104
|
+
ON CONFLICT(workdir_key, path) DO UPDATE SET
|
|
105
|
+
previous_kind = excluded.previous_kind,
|
|
106
|
+
previous_mode = excluded.previous_mode,
|
|
107
|
+
previous_hash = excluded.previous_hash,
|
|
108
|
+
current_kind = excluded.current_kind,
|
|
109
|
+
current_mode = excluded.current_mode,
|
|
110
|
+
current_hash = excluded.current_hash,
|
|
111
|
+
source_kind = excluded.source_kind,
|
|
112
|
+
source_path = excluded.source_path`);
|
|
113
|
+
const deleteChangeStmt = db.query("DELETE FROM workdir_changes WHERE workdir_key = ? AND path = ?");
|
|
114
|
+
const clearChangesStmt = db.query("DELETE FROM workdir_changes WHERE workdir_key = ?");
|
|
115
|
+
const listDirtyDirsStmt = db.query(`SELECT path, is_dirty, dirty_entry_count, dirty_descendant_count, affected_names, current_tree_hash, hash_state
|
|
116
|
+
FROM workdir_dirty_dirs
|
|
117
|
+
WHERE workdir_key = ?
|
|
118
|
+
ORDER BY path`);
|
|
119
|
+
const getDirtyDirStmt = db.query(`SELECT path, is_dirty, dirty_entry_count, dirty_descendant_count, affected_names, current_tree_hash, hash_state
|
|
120
|
+
FROM workdir_dirty_dirs
|
|
121
|
+
WHERE workdir_key = ? AND path = ?`);
|
|
122
|
+
const upsertDirtyDirStmt = db.query(`INSERT INTO workdir_dirty_dirs (
|
|
123
|
+
workdir_key, path, is_dirty, dirty_entry_count, dirty_descendant_count,
|
|
124
|
+
affected_names, current_tree_hash, hash_state
|
|
125
|
+
)
|
|
126
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
127
|
+
ON CONFLICT(workdir_key, path) DO UPDATE SET
|
|
128
|
+
is_dirty = excluded.is_dirty,
|
|
129
|
+
dirty_entry_count = excluded.dirty_entry_count,
|
|
130
|
+
dirty_descendant_count = excluded.dirty_descendant_count,
|
|
131
|
+
affected_names = excluded.affected_names,
|
|
132
|
+
current_tree_hash = excluded.current_tree_hash,
|
|
133
|
+
hash_state = excluded.hash_state`);
|
|
134
|
+
const deleteDirtyDirStmt = db.query("DELETE FROM workdir_dirty_dirs WHERE workdir_key = ? AND path = ?");
|
|
135
|
+
const clearDirtyDirsStmt = db.query("DELETE FROM workdir_dirty_dirs WHERE workdir_key = ?");
|
|
102
136
|
const resetTx = db.transaction((baseTree) => {
|
|
103
|
-
|
|
104
|
-
clearNodesStmt.run(
|
|
105
|
-
clearChangesStmt.run(
|
|
106
|
-
|
|
137
|
+
upsertWorkdirStmt.run(workdirKey, baseTree);
|
|
138
|
+
clearNodesStmt.run(workdirKey);
|
|
139
|
+
clearChangesStmt.run(workdirKey);
|
|
140
|
+
clearDirtyDirsStmt.run(workdirKey);
|
|
141
|
+
writeNode(setNodeStmt, workdirKey, createRootDirectoryNode(baseTree));
|
|
107
142
|
});
|
|
108
143
|
return {
|
|
109
144
|
kind: "sqlite",
|
|
@@ -111,32 +146,49 @@ function createSqliteVirtualWorkdirStateStore(db, sessionId) {
|
|
|
111
146
|
return transactImpl(fn);
|
|
112
147
|
},
|
|
113
148
|
readBaseTree() {
|
|
114
|
-
const row = readBaseTreeStmt.get(
|
|
115
|
-
if (row === null) throw new Error(`Virtual workdir
|
|
149
|
+
const row = readBaseTreeStmt.get(workdirKey);
|
|
150
|
+
if (row === null) throw new Error(`Virtual workdir not found: ${workdirKey}`);
|
|
116
151
|
return readBaseTreeValue(row.base_tree);
|
|
117
152
|
},
|
|
118
153
|
writeBaseTree(baseTree) {
|
|
119
|
-
|
|
154
|
+
upsertWorkdirStmt.run(workdirKey, baseTree);
|
|
120
155
|
},
|
|
121
156
|
getNode(id) {
|
|
122
|
-
const row = getNodeStmt.get(
|
|
157
|
+
const row = getNodeStmt.get(workdirKey, id);
|
|
123
158
|
if (row === null) return null;
|
|
124
159
|
return readNode(row);
|
|
125
160
|
},
|
|
126
161
|
setNode(node) {
|
|
127
|
-
writeNode(setNodeStmt,
|
|
162
|
+
writeNode(setNodeStmt, workdirKey, node);
|
|
128
163
|
},
|
|
129
164
|
deleteNode(id) {
|
|
130
|
-
deleteNodeStmt.run(
|
|
131
|
-
},
|
|
132
|
-
appendChange(record) {
|
|
133
|
-
insertChangeStmt.run(sessionId, record.op, getRecordPath(record), getRecordOldPath(record));
|
|
165
|
+
deleteNodeStmt.run(workdirKey, id);
|
|
134
166
|
},
|
|
135
167
|
listChangeRecords() {
|
|
136
|
-
return listChangesStmt.all(
|
|
168
|
+
return listChangesStmt.all(workdirKey).map(readChangeRecord);
|
|
169
|
+
},
|
|
170
|
+
getChangeRecord(path) {
|
|
171
|
+
const row = getChangeStmt.get(workdirKey, path);
|
|
172
|
+
return row === null ? null : readChangeRecord(row);
|
|
137
173
|
},
|
|
138
|
-
|
|
139
|
-
|
|
174
|
+
setChangeRecord(record) {
|
|
175
|
+
upsertChangeStmt.run(workdirKey, record.path, record.previous?.kind ?? null, record.previous?.mode ?? null, record.previous?.hash ?? null, record.current?.kind ?? null, record.current?.mode ?? null, record.current?.hash ?? null, record.source?.kind ?? null, record.source?.path ?? null);
|
|
176
|
+
},
|
|
177
|
+
deleteChangeRecord(path) {
|
|
178
|
+
deleteChangeStmt.run(workdirKey, path);
|
|
179
|
+
},
|
|
180
|
+
listDirtyDirSummaries() {
|
|
181
|
+
return listDirtyDirsStmt.all(workdirKey).map(readDirtyDirSummary);
|
|
182
|
+
},
|
|
183
|
+
getDirtyDirSummary(path) {
|
|
184
|
+
const row = getDirtyDirStmt.get(workdirKey, path);
|
|
185
|
+
return row === null ? null : readDirtyDirSummary(row);
|
|
186
|
+
},
|
|
187
|
+
setDirtyDirSummary(summary) {
|
|
188
|
+
upsertDirtyDirStmt.run(workdirKey, summary.path, summary.isDirty ? 1 : 0, summary.dirtyEntryCount, summary.dirtyDescendantCount, JSON.stringify(summary.affectedNames), summary.currentTreeHash, summary.hashState);
|
|
189
|
+
},
|
|
190
|
+
deleteDirtyDirSummary(path) {
|
|
191
|
+
deleteDirtyDirStmt.run(workdirKey, path);
|
|
140
192
|
},
|
|
141
193
|
reset(baseTree) {
|
|
142
194
|
resetTx(baseTree);
|
|
@@ -147,14 +199,14 @@ function ensureSchema(db) {
|
|
|
147
199
|
const currentVersion = readSchemaVersion(db);
|
|
148
200
|
if (currentVersion !== 0 && currentVersion !== WORKDIR_SQLITE_SCHEMA_VERSION) throw new Error(`Unsupported virtual workdir SQLite schema version: expected ${WORKDIR_SQLITE_SCHEMA_VERSION}, got ${currentVersion}`);
|
|
149
201
|
db.run(`
|
|
150
|
-
CREATE TABLE IF NOT EXISTS
|
|
151
|
-
|
|
202
|
+
CREATE TABLE IF NOT EXISTS workdirs (
|
|
203
|
+
workdir_key TEXT PRIMARY KEY,
|
|
152
204
|
base_tree TEXT NOT NULL
|
|
153
205
|
)
|
|
154
206
|
`);
|
|
155
207
|
db.run(`
|
|
156
208
|
CREATE TABLE IF NOT EXISTS workdir_nodes (
|
|
157
|
-
|
|
209
|
+
workdir_key TEXT NOT NULL,
|
|
158
210
|
node_id TEXT NOT NULL,
|
|
159
211
|
origin_kind TEXT NOT NULL,
|
|
160
212
|
origin_hash TEXT,
|
|
@@ -164,32 +216,48 @@ function ensureSchema(db) {
|
|
|
164
216
|
content BLOB,
|
|
165
217
|
target BLOB,
|
|
166
218
|
directory_overlay TEXT,
|
|
167
|
-
PRIMARY KEY (
|
|
219
|
+
PRIMARY KEY (workdir_key, node_id)
|
|
168
220
|
)
|
|
169
221
|
`);
|
|
170
222
|
db.run(`
|
|
171
223
|
CREATE TABLE IF NOT EXISTS workdir_changes (
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
224
|
+
workdir_key TEXT NOT NULL,
|
|
225
|
+
path TEXT NOT NULL,
|
|
226
|
+
previous_kind TEXT,
|
|
227
|
+
previous_mode TEXT,
|
|
228
|
+
previous_hash TEXT,
|
|
229
|
+
current_kind TEXT,
|
|
230
|
+
current_mode TEXT,
|
|
231
|
+
current_hash TEXT,
|
|
232
|
+
source_kind TEXT,
|
|
233
|
+
source_path TEXT,
|
|
234
|
+
PRIMARY KEY (workdir_key, path)
|
|
177
235
|
)
|
|
178
236
|
`);
|
|
179
237
|
db.run(`
|
|
180
|
-
CREATE
|
|
181
|
-
|
|
238
|
+
CREATE TABLE IF NOT EXISTS workdir_dirty_dirs (
|
|
239
|
+
workdir_key TEXT NOT NULL,
|
|
240
|
+
path TEXT NOT NULL,
|
|
241
|
+
is_dirty INTEGER NOT NULL,
|
|
242
|
+
dirty_entry_count INTEGER NOT NULL,
|
|
243
|
+
dirty_descendant_count INTEGER NOT NULL,
|
|
244
|
+
affected_names TEXT NOT NULL,
|
|
245
|
+
current_tree_hash TEXT,
|
|
246
|
+
hash_state TEXT NOT NULL,
|
|
247
|
+
PRIMARY KEY (workdir_key, path)
|
|
248
|
+
)
|
|
182
249
|
`);
|
|
183
250
|
writeSchemaVersion(db, WORKDIR_SQLITE_SCHEMA_VERSION);
|
|
184
251
|
}
|
|
185
|
-
function
|
|
186
|
-
return db.query("SELECT 1 FROM
|
|
252
|
+
function hasWorkdir(db, workdirKey) {
|
|
253
|
+
return db.query("SELECT 1 FROM workdirs WHERE workdir_key = ?").get(workdirKey) !== null;
|
|
187
254
|
}
|
|
188
|
-
function
|
|
255
|
+
function deleteWorkdirRows(db, workdirKey) {
|
|
189
256
|
db.transaction(() => {
|
|
190
|
-
db.query("DELETE FROM
|
|
191
|
-
db.query("DELETE FROM
|
|
192
|
-
db.query("DELETE FROM
|
|
257
|
+
db.query("DELETE FROM workdir_dirty_dirs WHERE workdir_key = ?").run(workdirKey);
|
|
258
|
+
db.query("DELETE FROM workdir_changes WHERE workdir_key = ?").run(workdirKey);
|
|
259
|
+
db.query("DELETE FROM workdir_nodes WHERE workdir_key = ?").run(workdirKey);
|
|
260
|
+
db.query("DELETE FROM workdirs WHERE workdir_key = ?").run(workdirKey);
|
|
193
261
|
})();
|
|
194
262
|
}
|
|
195
263
|
function readSchemaVersion(db) {
|
|
@@ -198,33 +266,33 @@ function readSchemaVersion(db) {
|
|
|
198
266
|
function writeSchemaVersion(db, version) {
|
|
199
267
|
db.run(`PRAGMA user_version = ${version}`);
|
|
200
268
|
}
|
|
201
|
-
function
|
|
202
|
-
const
|
|
203
|
-
if (
|
|
204
|
-
readBaseTreeValue(
|
|
269
|
+
function validateWorkdirIntegrity(db, workdirKey) {
|
|
270
|
+
const workdirRow = db.query("SELECT base_tree FROM workdirs WHERE workdir_key = ?").get(workdirKey);
|
|
271
|
+
if (workdirRow === null) throw new Error(`Virtual workdir not found: ${workdirKey}`);
|
|
272
|
+
readBaseTreeValue(workdirRow.base_tree);
|
|
205
273
|
const rootRow = db.query(`SELECT node_id, origin_kind, origin_hash, origin_mode, state_kind, state_mode, content, target, directory_overlay
|
|
206
274
|
FROM workdir_nodes
|
|
207
|
-
WHERE
|
|
208
|
-
if (rootRow === null) throw new Error(`Virtual workdir
|
|
209
|
-
if (readNode(rootRow).state.kind !== "directory") throw new Error(`Virtual workdir
|
|
275
|
+
WHERE workdir_key = ? AND node_id = ?`).get(workdirKey, "root");
|
|
276
|
+
if (rootRow === null) throw new Error(`Virtual workdir is corrupted: missing root node for ${workdirKey}`);
|
|
277
|
+
if (readNode(rootRow).state.kind !== "directory") throw new Error(`Virtual workdir is corrupted: root node is not a directory for ${workdirKey}`);
|
|
210
278
|
const allNodesStmt = db.query(`SELECT node_id, origin_kind, origin_hash, origin_mode, state_kind, state_mode, content, target, directory_overlay
|
|
211
279
|
FROM workdir_nodes
|
|
212
|
-
WHERE
|
|
213
|
-
for (const row of allNodesStmt.all(
|
|
280
|
+
WHERE workdir_key = ?`);
|
|
281
|
+
for (const row of allNodesStmt.all(workdirKey)) readNode(row);
|
|
214
282
|
}
|
|
215
|
-
function writeNode(stmt,
|
|
283
|
+
function writeNode(stmt, workdirKey, node) {
|
|
216
284
|
if (node.state.kind === "directory") {
|
|
217
|
-
stmt.run(
|
|
285
|
+
stmt.run(workdirKey, node.id, node.origin.kind, node.origin.kind === "none" ? null : node.origin.hash, node.origin.kind === "repo-blob" ? node.origin.mode : null, "directory", null, null, null, JSON.stringify({
|
|
218
286
|
addedEntries: Array.from(node.state.overlay.addedEntries.entries()),
|
|
219
287
|
deletedNames: Array.from(node.state.overlay.deletedNames.values())
|
|
220
288
|
}));
|
|
221
289
|
return;
|
|
222
290
|
}
|
|
223
291
|
if (node.state.kind === "file") {
|
|
224
|
-
stmt.run(
|
|
292
|
+
stmt.run(workdirKey, node.id, node.origin.kind, node.origin.kind === "none" ? null : node.origin.hash, node.origin.kind === "repo-blob" ? node.origin.mode : null, "file", node.state.mode, node.state.content ?? null, null, null);
|
|
225
293
|
return;
|
|
226
294
|
}
|
|
227
|
-
stmt.run(
|
|
295
|
+
stmt.run(workdirKey, node.id, node.origin.kind, node.origin.kind === "none" ? null : node.origin.hash, node.origin.kind === "repo-blob" ? node.origin.mode : null, "symlink", "120000", null, node.state.target ?? null, null);
|
|
228
296
|
}
|
|
229
297
|
function readNode(row) {
|
|
230
298
|
const origin = readNodeOrigin(row);
|
|
@@ -286,6 +354,67 @@ function readNodeOrigin(row) {
|
|
|
286
354
|
}
|
|
287
355
|
throw new Error(`Invalid SQLite workdir node origin kind: ${row.origin_kind}`);
|
|
288
356
|
}
|
|
357
|
+
function readChangeRecord(row) {
|
|
358
|
+
return {
|
|
359
|
+
path: row.path,
|
|
360
|
+
previous: row.previous_kind === null || row.previous_mode === null || row.previous_hash === null ? null : {
|
|
361
|
+
kind: readDiffObjectKind(row.previous_kind),
|
|
362
|
+
mode: readDiffObjectMode(row.previous_mode),
|
|
363
|
+
hash: row.previous_hash
|
|
364
|
+
},
|
|
365
|
+
current: row.current_kind === null || row.current_mode === null || row.current_hash === null ? null : {
|
|
366
|
+
kind: readDiffObjectKind(row.current_kind),
|
|
367
|
+
mode: readDiffObjectMode(row.current_mode),
|
|
368
|
+
hash: row.current_hash
|
|
369
|
+
},
|
|
370
|
+
source: row.source_kind === null || row.source_path === null ? null : {
|
|
371
|
+
kind: readDiffSourceKind(row.source_kind),
|
|
372
|
+
path: row.source_path
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function readDiffObjectKind(raw) {
|
|
377
|
+
if (raw === "blob" || raw === "symlink") return raw;
|
|
378
|
+
throw new Error(`Invalid SQLite workdir diff object kind: ${raw}`);
|
|
379
|
+
}
|
|
380
|
+
function readDiffObjectMode(raw) {
|
|
381
|
+
if (raw === "100644" || raw === "100755" || raw === "120000") return raw;
|
|
382
|
+
throw new Error(`Invalid SQLite workdir diff object mode: ${raw}`);
|
|
383
|
+
}
|
|
384
|
+
function readDiffSourceKind(raw) {
|
|
385
|
+
if (raw === "rename" || raw === "copy") return raw;
|
|
386
|
+
throw new Error(`Invalid SQLite workdir diff source kind: ${raw}`);
|
|
387
|
+
}
|
|
388
|
+
function readDirtyDirSummary(row) {
|
|
389
|
+
const affectedNames = readDirtyDirAffectedNames(row.affected_names);
|
|
390
|
+
return {
|
|
391
|
+
path: row.path,
|
|
392
|
+
isDirty: row.is_dirty !== 0,
|
|
393
|
+
dirtyEntryCount: readDirtyDirCount(row.dirty_entry_count, "dirty_entry_count"),
|
|
394
|
+
dirtyDescendantCount: readDirtyDirCount(row.dirty_descendant_count, "dirty_descendant_count"),
|
|
395
|
+
affectedNames,
|
|
396
|
+
currentTreeHash: row.current_tree_hash === null ? null : sha1(row.current_tree_hash),
|
|
397
|
+
hashState: readDirtyDirHashState(row.hash_state)
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
function readDirtyDirCount(raw, field) {
|
|
401
|
+
if (!Number.isInteger(raw) || raw < 0) throw new Error(`Invalid SQLite workdir dirty dir ${field}: ${raw}`);
|
|
402
|
+
return raw;
|
|
403
|
+
}
|
|
404
|
+
function readDirtyDirAffectedNames(raw) {
|
|
405
|
+
let parsed;
|
|
406
|
+
try {
|
|
407
|
+
parsed = JSON.parse(raw);
|
|
408
|
+
} catch {
|
|
409
|
+
throw new Error("Invalid SQLite workdir dirty dir affected_names JSON");
|
|
410
|
+
}
|
|
411
|
+
if (!Array.isArray(parsed) || parsed.some((item) => typeof item !== "string")) throw new Error("Invalid SQLite workdir dirty dir affected_names payload");
|
|
412
|
+
return [...parsed].sort((left, right) => left.localeCompare(right));
|
|
413
|
+
}
|
|
414
|
+
function readDirtyDirHashState(raw) {
|
|
415
|
+
if (raw === "stale" || raw === "materialized") return raw;
|
|
416
|
+
throw new Error(`Invalid SQLite workdir dirty dir hash state: ${raw}`);
|
|
417
|
+
}
|
|
289
418
|
function readDirectoryOverlay(raw) {
|
|
290
419
|
if (raw === null) return {
|
|
291
420
|
addedEntries: /* @__PURE__ */ new Map(),
|
|
@@ -311,11 +440,11 @@ function readBlobColumn(raw, column) {
|
|
|
311
440
|
return Buffer.from(raw);
|
|
312
441
|
}
|
|
313
442
|
function readBaseTreeValue(raw) {
|
|
314
|
-
if (typeof raw !== "string") throw new Error("Invalid SQLite workdir
|
|
443
|
+
if (typeof raw !== "string") throw new Error("Invalid SQLite workdir base_tree");
|
|
315
444
|
try {
|
|
316
445
|
return sha1(raw);
|
|
317
446
|
} catch {
|
|
318
|
-
throw new Error("Invalid SQLite workdir
|
|
447
|
+
throw new Error("Invalid SQLite workdir base_tree");
|
|
319
448
|
}
|
|
320
449
|
}
|
|
321
450
|
function isDirectoryOverlayPayload(value) {
|
|
@@ -326,38 +455,5 @@ function isDirectoryOverlayPayload(value) {
|
|
|
326
455
|
const hasValidDeletedNames = maybe.deletedNames.every((name) => typeof name === "string");
|
|
327
456
|
return hasValidAddedEntries && hasValidDeletedNames;
|
|
328
457
|
}
|
|
329
|
-
function getRecordPath(record) {
|
|
330
|
-
switch (record.op) {
|
|
331
|
-
case "add":
|
|
332
|
-
case "modify":
|
|
333
|
-
case "delete":
|
|
334
|
-
case "revert": return record.path;
|
|
335
|
-
case "rename":
|
|
336
|
-
case "copy": return record.to;
|
|
337
|
-
default: return record;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
function getRecordOldPath(record) {
|
|
341
|
-
if (record.op === "rename" || record.op === "copy") return record.from;
|
|
342
|
-
return null;
|
|
343
|
-
}
|
|
344
|
-
function readChangeRecord(row) {
|
|
345
|
-
switch (row.op) {
|
|
346
|
-
case "add":
|
|
347
|
-
case "modify":
|
|
348
|
-
case "delete":
|
|
349
|
-
case "revert": return {
|
|
350
|
-
op: row.op,
|
|
351
|
-
path: row.path
|
|
352
|
-
};
|
|
353
|
-
case "rename":
|
|
354
|
-
case "copy": return {
|
|
355
|
-
op: row.op,
|
|
356
|
-
from: row.old_path,
|
|
357
|
-
to: row.path
|
|
358
|
-
};
|
|
359
|
-
default: throw new Error(`Unknown workdir change op: ${row.op}`);
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
458
|
//#endregion
|
|
363
|
-
export {
|
|
459
|
+
export { deleteSqliteVirtualWorkdir, openSqliteVirtualWorkdir };
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { type
|
|
1
|
+
import { OpenSqliteVirtualWorkdirOptions, SqliteVirtualWorkdir, deleteSqliteVirtualWorkdir, openSqliteVirtualWorkdir } from "./sqlite-backend.mjs";
|
|
2
|
+
export { type OpenSqliteVirtualWorkdirOptions, type SqliteVirtualWorkdir, deleteSqliteVirtualWorkdir, openSqliteVirtualWorkdir };
|
package/dist/workdir/sqlite.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export {
|
|
1
|
+
import { deleteSqliteVirtualWorkdir, openSqliteVirtualWorkdir } from "./sqlite-backend.mjs";
|
|
2
|
+
export { deleteSqliteVirtualWorkdir, openSqliteVirtualWorkdir };
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { SHA1 } from "../core/types.mjs";
|
|
2
|
-
import { InternalChangeRecord } from "./change-log.mjs";
|
|
3
2
|
import { NodeId } from "./ids.mjs";
|
|
4
|
-
import {
|
|
3
|
+
import { NormalizedChangeRecord } from "./change-index.mjs";
|
|
4
|
+
import { DirtyDirSummary } from "./dirty-dir.mjs";
|
|
5
|
+
import { WorkdirNode } from "./nodes.mjs";
|
|
5
6
|
|
|
6
7
|
//#region src/workdir/state-store.d.ts
|
|
7
8
|
/**
|
|
@@ -13,7 +14,7 @@ interface VirtualWorkdirStateStore {
|
|
|
13
14
|
/**
|
|
14
15
|
* 在单次提交边界内执行状态变更
|
|
15
16
|
*
|
|
16
|
-
* 用于把一次
|
|
17
|
+
* 用于把一次 workdir 写操作封装为单个内部事务。
|
|
17
18
|
* 若回调抛错,store 应尽力恢复到调用前状态。
|
|
18
19
|
*/
|
|
19
20
|
transact<T>(fn: () => T): T;
|
|
@@ -22,21 +23,31 @@ interface VirtualWorkdirStateStore {
|
|
|
22
23
|
/** 覆盖当前基线 tree */
|
|
23
24
|
writeBaseTree(baseTree: SHA1): void;
|
|
24
25
|
/** 读取节点,不存在时返回 null */
|
|
25
|
-
getNode(id: NodeId):
|
|
26
|
+
getNode(id: NodeId): WorkdirNode | null;
|
|
26
27
|
/** 写入或覆盖节点 */
|
|
27
|
-
setNode(node:
|
|
28
|
+
setNode(node: WorkdirNode): void;
|
|
28
29
|
/** 删除节点 */
|
|
29
30
|
deleteNode(id: NodeId): void;
|
|
30
|
-
/**
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
|
|
31
|
+
/** 列出全部规范化变更记录 */
|
|
32
|
+
listChangeRecords(): readonly NormalizedChangeRecord[];
|
|
33
|
+
/** 按路径读取规范化变更记录 */
|
|
34
|
+
getChangeRecord(path: string): NormalizedChangeRecord | null;
|
|
35
|
+
/** 写入或覆盖规范化变更记录 */
|
|
36
|
+
setChangeRecord(record: NormalizedChangeRecord): void;
|
|
37
|
+
/** 删除规范化变更记录 */
|
|
38
|
+
deleteChangeRecord(path: string): void;
|
|
39
|
+
/** 列出全部脏目录摘要 */
|
|
40
|
+
listDirtyDirSummaries(): readonly DirtyDirSummary[];
|
|
41
|
+
/** 按目录路径读取脏目录摘要 */
|
|
42
|
+
getDirtyDirSummary(path: string): DirtyDirSummary | null;
|
|
43
|
+
/** 写入或覆盖脏目录摘要 */
|
|
44
|
+
setDirtyDirSummary(summary: DirtyDirSummary): void;
|
|
45
|
+
/** 删除脏目录摘要 */
|
|
46
|
+
deleteDirtyDirSummary(path: string): void;
|
|
36
47
|
/**
|
|
37
48
|
* 重置为新的基线 tree
|
|
38
49
|
*
|
|
39
|
-
*
|
|
50
|
+
* 需要同时清空节点状态,并重新建立根节点。
|
|
40
51
|
*/
|
|
41
52
|
reset(baseTree: SHA1): void;
|
|
42
53
|
}
|