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.
Files changed (39) hide show
  1. package/README.md +6 -0
  2. package/dist/workdir/change-index-plan.mjs +76 -0
  3. package/dist/workdir/change-index.d.mts +21 -0
  4. package/dist/workdir/change-index.mjs +413 -0
  5. package/dist/workdir/core.d.mts +64 -63
  6. package/dist/workdir/directory-view.mjs +197 -0
  7. package/dist/workdir/dirty-dir-plan.mjs +73 -0
  8. package/dist/workdir/dirty-dir.d.mts +28 -0
  9. package/dist/workdir/dirty-dir.mjs +32 -0
  10. package/dist/workdir/file-backend.d.mts +34 -12
  11. package/dist/workdir/file-backend.mjs +161 -94
  12. package/dist/workdir/file.d.mts +2 -2
  13. package/dist/workdir/file.mjs +2 -2
  14. package/dist/workdir/ids.d.mts +1 -1
  15. package/dist/workdir/ids.mjs +3 -3
  16. package/dist/workdir/memory-backend.mjs +34 -50
  17. package/dist/workdir/memory.d.mts +2 -3
  18. package/dist/workdir/memory.mjs +2 -3
  19. package/dist/workdir/nodes.d.mts +7 -7
  20. package/dist/workdir/nodes.mjs +3 -9
  21. package/dist/workdir/overlay.d.mts +3 -3
  22. package/dist/workdir/sqlite-backend.d.mts +31 -14
  23. package/dist/workdir/sqlite-backend.mjs +238 -142
  24. package/dist/workdir/sqlite.d.mts +2 -2
  25. package/dist/workdir/sqlite.mjs +2 -2
  26. package/dist/workdir/state-store.d.mts +23 -12
  27. package/dist/workdir/workdir-path.mjs +348 -0
  28. package/dist/workdir/workdir-transaction.mjs +115 -0
  29. package/dist/workdir/workdir.d.mts +30 -0
  30. package/dist/workdir/workdir.mjs +280 -0
  31. package/dist/workdir/write-tree.mjs +108 -44
  32. package/package.json +2 -1
  33. package/dist/workdir/change-log.d.mts +0 -25
  34. package/dist/workdir/change-log.mjs +0 -69
  35. package/dist/workdir/memory-backend.d.mts +0 -17
  36. package/dist/workdir/session-id.mjs +0 -18
  37. package/dist/workdir/session-internal.mjs +0 -141
  38. package/dist/workdir/session.d.mts +0 -30
  39. package/dist/workdir/session.mjs +0 -407
@@ -1,30 +1,47 @@
1
- import { VirtualWorkdirBackend } from "./core.mjs";
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
- /** 创建 SQLite Virtual Workdir backend 的可选参数 */
6
- interface CreateSqliteVirtualWorkdirBackendOptions {
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 Virtual Workdir backend(含资源释放能力)
17
+ * 基于 SQLite VirtualWorkdir
18
+ *
19
+ * 返回值附带 `[Symbol.dispose]()`,用于释放内部数据库连接。
12
20
  */
13
- interface SqliteVirtualWorkdirBackend extends VirtualWorkdirBackend {
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
- * 创建基于 SQLite Virtual Workdir backend
38
+ * 删除指定 key 上的 SQLite VirtualWorkdir
19
39
  *
20
40
  * @example
21
41
  * ```ts
22
- * using backend = createSqliteVirtualWorkdirBackend(":memory:");
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 createSqliteVirtualWorkdirBackend(dbPath: string, options?: CreateSqliteVirtualWorkdirBackendOptions): SqliteVirtualWorkdirBackend;
45
+ declare function deleteSqliteVirtualWorkdir(dbPath: string, workdirKey: string, options?: SqliteVirtualWorkdirConnectionOptions): void;
29
46
  //#endregion
30
- export { CreateSqliteVirtualWorkdirBackendOptions, SqliteVirtualWorkdirBackend, createSqliteVirtualWorkdirBackend };
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 { createVirtualWorkdirSessionId } from "./session-id.mjs";
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 = 1;
11
- const WORKDIR_CHANGES_SESSION_ORDER_INDEX = "workdir_changes_session_id_id_idx";
9
+ const WORKDIR_SQLITE_SCHEMA_VERSION = 6;
12
10
  /**
13
- * 创建基于 SQLite Virtual Workdir backend
11
+ * 打开基于 SQLite 的持久化 VirtualWorkdir
14
12
  *
15
13
  * @example
16
14
  * ```ts
17
- * using backend = createSqliteVirtualWorkdirBackend(":memory:");
18
- * const sessionId = backend.createSession({ baseTree: tree });
19
- * const session = backend.openSession(repo.objects, sessionId);
20
- * expect(session.baseTree).toBe(tree);
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 createSqliteVirtualWorkdirBackend(dbPath, options = {}) {
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
- return {
29
- kind: "sqlite",
30
- createSession(options) {
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
- function assertBackendAvailable(disposed) {
66
- if (disposed) throw new Error("SQLite virtual workdir backend is disposed");
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
- * 创建单个 session SQLite 状态存储
63
+ * 创建单个 SQLite VirtualWorkdir 的状态存储
70
64
  *
71
65
  * @example
72
66
  * ```ts
73
- * const store = createSqliteVirtualWorkdirStateStore(db, sessionId);
67
+ * const store = createSqliteVirtualWorkdirStateStore(db, "demo");
74
68
  * expect(store.kind).toBe("sqlite");
75
69
  * ```
76
70
  */
77
- function createSqliteVirtualWorkdirStateStore(db, sessionId) {
71
+ function createSqliteVirtualWorkdirStateStore(db, workdirKey) {
78
72
  const transactImpl = db.transaction((fn) => fn());
79
- const readBaseTreeStmt = db.query("SELECT base_tree FROM workdir_sessions WHERE session_id = ?");
80
- const upsertSessionStmt = db.query("INSERT INTO workdir_sessions (session_id, base_tree) VALUES (?, ?) ON CONFLICT(session_id) DO UPDATE SET base_tree = excluded.base_tree");
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 session_id = ? AND node_id = ?`);
77
+ WHERE workdir_key = ? AND node_id = ?`);
84
78
  const setNodeStmt = db.query(`INSERT INTO workdir_nodes (
85
- session_id, node_id, origin_kind, origin_hash, origin_mode,
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(session_id, node_id) DO UPDATE SET
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 session_id = ? AND node_id = ?");
98
- const listChangesStmt = db.query("SELECT op, path, old_path FROM workdir_changes WHERE session_id = ? ORDER BY id");
99
- const insertChangeStmt = db.query("INSERT INTO workdir_changes (session_id, op, path, old_path) VALUES (?, ?, ?, ?)");
100
- const clearNodesStmt = db.query("DELETE FROM workdir_nodes WHERE session_id = ?");
101
- const clearChangesStmt = db.query("DELETE FROM workdir_changes WHERE session_id = ?");
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
- upsertSessionStmt.run(sessionId, baseTree);
104
- clearNodesStmt.run(sessionId);
105
- clearChangesStmt.run(sessionId);
106
- writeNode(setNodeStmt, sessionId, createRootDirectoryNode(baseTree));
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(sessionId);
115
- if (row === null) throw new Error(`Virtual workdir session not found: ${sessionId}`);
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
- upsertSessionStmt.run(sessionId, baseTree);
154
+ upsertWorkdirStmt.run(workdirKey, baseTree);
120
155
  },
121
156
  getNode(id) {
122
- const row = getNodeStmt.get(sessionId, id);
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, sessionId, node);
162
+ writeNode(setNodeStmt, workdirKey, node);
128
163
  },
129
164
  deleteNode(id) {
130
- deleteNodeStmt.run(sessionId, id);
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(sessionId).map(readChangeRecord);
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
- clearChanges() {
139
- clearChangesStmt.run(sessionId);
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 workdir_sessions (
151
- session_id TEXT PRIMARY KEY,
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
- session_id TEXT NOT NULL,
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 (session_id, node_id)
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
- id INTEGER PRIMARY KEY AUTOINCREMENT,
173
- session_id TEXT NOT NULL,
174
- op TEXT NOT NULL,
175
- path TEXT,
176
- old_path TEXT
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 INDEX IF NOT EXISTS ${WORKDIR_CHANGES_SESSION_ORDER_INDEX}
181
- ON workdir_changes (session_id, id)
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 hasSession(db, sessionId) {
186
- return db.query("SELECT 1 FROM workdir_sessions WHERE session_id = ?").get(sessionId) !== null;
252
+ function hasWorkdir(db, workdirKey) {
253
+ return db.query("SELECT 1 FROM workdirs WHERE workdir_key = ?").get(workdirKey) !== null;
187
254
  }
188
- function deleteSessionRows(db, sessionId) {
255
+ function deleteWorkdirRows(db, workdirKey) {
189
256
  db.transaction(() => {
190
- db.query("DELETE FROM workdir_changes WHERE session_id = ?").run(sessionId);
191
- db.query("DELETE FROM workdir_nodes WHERE session_id = ?").run(sessionId);
192
- db.query("DELETE FROM workdir_sessions WHERE session_id = ?").run(sessionId);
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 validateSessionIntegrity(db, sessionId) {
202
- const sessionRow = db.query("SELECT base_tree FROM workdir_sessions WHERE session_id = ?").get(sessionId);
203
- if (sessionRow === null) throw new Error(`Virtual workdir session not found: ${sessionId}`);
204
- readBaseTreeValue(sessionRow.base_tree);
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 session_id = ? AND node_id = ?`).get(sessionId, "root");
208
- if (rootRow === null) throw new Error(`Virtual workdir session is corrupted: missing root node for ${sessionId}`);
209
- if (readNode(rootRow).state.kind !== "directory") throw new Error(`Virtual workdir session is corrupted: root node is not a directory for ${sessionId}`);
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 session_id = ?`);
213
- for (const row of allNodesStmt.all(sessionId)) readNode(row);
280
+ WHERE workdir_key = ?`);
281
+ for (const row of allNodesStmt.all(workdirKey)) readNode(row);
214
282
  }
215
- function writeNode(stmt, sessionId, node) {
283
+ function writeNode(stmt, workdirKey, node) {
216
284
  if (node.state.kind === "directory") {
217
- stmt.run(sessionId, 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({
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(sessionId, 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);
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(sessionId, 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);
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 session base_tree");
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 session base_tree");
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 { createSqliteVirtualWorkdirBackend };
459
+ export { deleteSqliteVirtualWorkdir, openSqliteVirtualWorkdir };
@@ -1,2 +1,2 @@
1
- import { CreateSqliteVirtualWorkdirBackendOptions, SqliteVirtualWorkdirBackend, createSqliteVirtualWorkdirBackend } from "./sqlite-backend.mjs";
2
- export { type CreateSqliteVirtualWorkdirBackendOptions, type SqliteVirtualWorkdirBackend, createSqliteVirtualWorkdirBackend };
1
+ import { OpenSqliteVirtualWorkdirOptions, SqliteVirtualWorkdir, deleteSqliteVirtualWorkdir, openSqliteVirtualWorkdir } from "./sqlite-backend.mjs";
2
+ export { type OpenSqliteVirtualWorkdirOptions, type SqliteVirtualWorkdir, deleteSqliteVirtualWorkdir, openSqliteVirtualWorkdir };
@@ -1,2 +1,2 @@
1
- import { createSqliteVirtualWorkdirBackend } from "./sqlite-backend.mjs";
2
- export { createSqliteVirtualWorkdirBackend };
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 { SessionNode } from "./nodes.mjs";
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
- * 用于把一次 session 写操作封装为单个内部事务。
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): SessionNode | null;
26
+ getNode(id: NodeId): WorkdirNode | null;
26
27
  /** 写入或覆盖节点 */
27
- setNode(node: SessionNode): void;
28
+ setNode(node: WorkdirNode): void;
28
29
  /** 删除节点 */
29
30
  deleteNode(id: NodeId): void;
30
- /** 追加内部变更记录 */
31
- appendChange(record: InternalChangeRecord): void;
32
- /** 列出内部变更记录快照 */
33
- listChangeRecords(): readonly InternalChangeRecord[];
34
- /** 清空变更记录 */
35
- clearChanges(): void;
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
  }