nano-git 0.2.3 → 0.3.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.
@@ -1,11 +1,14 @@
1
1
  import { VirtualNotDirectoryError, VirtualNotFileError, VirtualNotSymlinkError, VirtualPathAlreadyExistsError, VirtualPathNotFoundError, VirtualRevertNotSupportedError } from "../core/errors.mjs";
2
- import { mapInternalChangesToVirtualChanges } from "./change-log.mjs";
3
- import { VIRTUAL_ROOT_NODE_ID, createNodeId } from "./ids.mjs";
2
+ import { createNodeId } from "./ids.mjs";
4
3
  import { overlayBindEntry, overlayRenameEntry, overlayTombstoneEntry } from "./overlay.mjs";
5
- import { cloneSessionNodeForCopy, revertNodeState } from "./nodes.mjs";
4
+ import { revertNodeState } from "./nodes.mjs";
6
5
  import { modeToVirtualEntryKind, readRepoBlobContent } from "./origin.mjs";
7
- import { assertValidVirtualPath, baseName, normalizeDirectoryPath, parentPath } from "./path.mjs";
8
- import { listDirectoryChildren, resolveChild, resolvePath } from "./session-internal.mjs";
6
+ import { assertValidVirtualPath, normalizeDirectoryPath } from "./path.mjs";
7
+ import { getDirectoryChildrenView, getRootNode, requireExistingWriteTarget, requireMissingWriteTarget, resolveLeafWriteTarget, resolvePath, resolveWriteTransfer } from "./session-internal.mjs";
8
+ import { createChangeIndexPlanner } from "./change-index-plan.mjs";
9
+ import { computeVirtualDiff, rebuildNormalizedChangeIndex, refreshChangeRecordForPath, replaceChangeRecords, rewriteChangeRecordForRename, writeChangeRecordForCopy } from "./change-index.mjs";
10
+ import { createDirtyDirPlanner } from "./dirty-dir-plan.mjs";
11
+ import { cloneNodeGraphForCopy, runInWriteTransaction, statDirectoryNode, statNode, updateParentOverlay } from "./session-transaction.mjs";
9
12
  import { writeTreeFromSession } from "./write-tree.mjs";
10
13
  import { createVirtualWorkdirMemoryStateStore } from "./memory-backend.mjs";
11
14
  //#region src/workdir/session.ts
@@ -39,6 +42,54 @@ function createVirtualWorkdirSession(source, options) {
39
42
  * ```
40
43
  */
41
44
  function openVirtualWorkdirSession(source, state) {
45
+ const currentNodeHashes = /* @__PURE__ */ new Map();
46
+ const invalidateDiffCaches = () => {
47
+ currentNodeHashes.clear();
48
+ };
49
+ const refreshChangeIndex = () => {
50
+ invalidateDiffCaches();
51
+ replaceChangeRecords(state, rebuildNormalizedChangeIndex(source, state, {
52
+ currentNodeHashes,
53
+ setCurrentNodeHash(nodeId, hash) {
54
+ currentNodeHashes.set(nodeId, hash);
55
+ }
56
+ }));
57
+ };
58
+ const refreshChangeIndexForPath = (path) => {
59
+ invalidateDiffCaches();
60
+ refreshChangeRecordForPath(source, state, path, {
61
+ currentNodeHashes,
62
+ setCurrentNodeHash(nodeId, hash) {
63
+ currentNodeHashes.set(nodeId, hash);
64
+ }
65
+ });
66
+ };
67
+ const rewriteChangeIndexForRename = (from, to) => {
68
+ invalidateDiffCaches();
69
+ rewriteChangeRecordForRename(source, state, from, to, {
70
+ currentNodeHashes,
71
+ setCurrentNodeHash(nodeId, hash) {
72
+ currentNodeHashes.set(nodeId, hash);
73
+ }
74
+ });
75
+ };
76
+ const writeChangeIndexForCopy = (from, to) => {
77
+ invalidateDiffCaches();
78
+ writeChangeRecordForCopy(source, state, from, to, {
79
+ currentNodeHashes,
80
+ setCurrentNodeHash(nodeId, hash) {
81
+ currentNodeHashes.set(nodeId, hash);
82
+ }
83
+ });
84
+ };
85
+ const changeIndexPlanner = createChangeIndexPlanner(source, state, {
86
+ rebuildAll: refreshChangeIndex,
87
+ refreshPath: refreshChangeIndexForPath,
88
+ rewriteRename: rewriteChangeIndexForRename,
89
+ writeCopy: writeChangeIndexForCopy
90
+ });
91
+ const dirtyDirPlanner = createDirtyDirPlanner(source, state);
92
+ refreshChangeIndex();
42
93
  return {
43
94
  get baseTree() {
44
95
  return state.readBaseTree();
@@ -61,7 +112,7 @@ function openVirtualWorkdirSession(source, state) {
61
112
  } : resolvePath(source, state, normalized);
62
113
  if (!resolved.found || resolved.node === null) throw new VirtualPathNotFoundError(normalized);
63
114
  if (resolved.node.state.kind !== "directory") throw new VirtualNotDirectoryError(normalized);
64
- return listDirectoryChildren(source, state, resolved.node, normalized).map((child) => ({
115
+ return getDirectoryChildrenView(source, state, resolved.node, normalized).children.map((child) => ({
65
116
  name: child.name,
66
117
  kind: modeToVirtualEntryKind(child.mode),
67
118
  mode: child.mode
@@ -91,19 +142,8 @@ function openVirtualWorkdirSession(source, state) {
91
142
  throw new VirtualPathNotFoundError(path);
92
143
  },
93
144
  mkdir(path) {
94
- runInWriteTransaction(state, () => {
95
- assertValidVirtualPath(path);
96
- const parent = parentPath(path);
97
- const name = baseName(path);
98
- const parentResolved = parent !== null ? resolvePath(source, state, parent) : {
99
- found: true,
100
- node: getRootNode(state)
101
- };
102
- if (!parentResolved.found || parentResolved.node === null) throw new VirtualPathNotFoundError(parent ?? path);
103
- const parentNode = parentResolved.node;
104
- if (parentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(parent ?? path);
105
- const existing = resolveChild(source, state, parentNode, parent ?? "", name);
106
- if (existing.found && existing.node !== null) throw new VirtualPathAlreadyExistsError(path);
145
+ runInWriteTransaction(state, () => dirtyDirPlanner.rebuild([path]), invalidateDiffCaches, () => {
146
+ const target = requireMissingWriteTarget(source, state, path);
107
147
  const nodeId = createNodeId();
108
148
  const newNode = {
109
149
  id: nodeId,
@@ -117,35 +157,20 @@ function openVirtualWorkdirSession(source, state) {
117
157
  }
118
158
  };
119
159
  state.setNode(newNode);
120
- updateParentOverlay(state, parentNode.id, overlayBindEntry(parentNode.state.overlay, name, nodeId));
121
- state.appendChange({
122
- op: "add",
123
- path
124
- });
160
+ updateParentOverlay(state, target.parentNode.id, overlayBindEntry(target.parentNode.state.overlay, target.name, nodeId));
125
161
  });
126
162
  },
127
163
  writeFile(path, content, options) {
128
164
  runInWriteTransaction(state, () => {
129
- assertValidVirtualPath(path);
165
+ changeIndexPlanner.apply(changeIndexPlanner.planRefreshForPath(path));
166
+ dirtyDirPlanner.rebuild([path]);
167
+ }, invalidateDiffCaches, () => {
130
168
  const mode = options?.mode ?? "100644";
131
- const parent = parentPath(path);
132
- const name = baseName(path);
133
- const parentResolved = parent !== null ? resolvePath(source, state, parent) : {
134
- found: true,
135
- node: getRootNode(state)
136
- };
137
- if (!parentResolved.found || parentResolved.node === null) throw new VirtualPathNotFoundError(parent ?? path);
138
- const parentNode = parentResolved.node;
139
- if (parentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(parent ?? path);
140
- const existing = resolveChild(source, state, parentNode, parent ?? "", name);
141
- if (existing.found && existing.node !== null) {
142
- if (existing.node.state.kind === "directory") throw new VirtualNotFileError(path);
143
- }
144
- const isNew = !existing.found || existing.node === null;
145
- const nodeId = existing.found ? existing.node.id : createNodeId();
169
+ const target = resolveLeafWriteTarget(source, state, path);
170
+ const nodeId = target.existing !== null ? target.existing.node.id : createNodeId();
146
171
  const fileNode = {
147
172
  id: nodeId,
148
- origin: existing.found ? existing.node.origin : { kind: "none" },
173
+ origin: target.existing !== null ? target.existing.node.origin : { kind: "none" },
149
174
  state: {
150
175
  kind: "file",
151
176
  mode,
@@ -153,34 +178,19 @@ function openVirtualWorkdirSession(source, state) {
153
178
  }
154
179
  };
155
180
  state.setNode(fileNode);
156
- updateParentOverlay(state, parentNode.id, overlayBindEntry(parentNode.state.overlay, name, nodeId));
157
- state.appendChange({
158
- op: isNew ? "add" : "modify",
159
- path
160
- });
181
+ if (target.existing === null || target.parentNode.state.overlay.addedEntries.has(target.name)) updateParentOverlay(state, target.parentNode.id, overlayBindEntry(target.parentNode.state.overlay, target.name, nodeId));
161
182
  });
162
183
  },
163
184
  writeLink(path, target) {
164
185
  runInWriteTransaction(state, () => {
165
- assertValidVirtualPath(path);
166
- const parent = parentPath(path);
167
- const name = baseName(path);
168
- const parentResolved = parent !== null ? resolvePath(source, state, parent) : {
169
- found: true,
170
- node: getRootNode(state)
171
- };
172
- if (!parentResolved.found || parentResolved.node === null) throw new VirtualPathNotFoundError(parent ?? path);
173
- const parentNode = parentResolved.node;
174
- if (parentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(parent ?? path);
175
- const existing = resolveChild(source, state, parentNode, parent ?? "", name);
176
- if (existing.found && existing.node !== null) {
177
- if (existing.node.state.kind === "directory") throw new VirtualNotFileError(path);
178
- }
179
- const isNew = !existing.found || existing.node === null;
180
- const nodeId = existing.found ? existing.node.id : createNodeId();
186
+ changeIndexPlanner.apply(changeIndexPlanner.planRefreshForPath(path));
187
+ dirtyDirPlanner.rebuild([path]);
188
+ }, invalidateDiffCaches, () => {
189
+ const writeTarget = resolveLeafWriteTarget(source, state, path);
190
+ const nodeId = writeTarget.existing !== null ? writeTarget.existing.node.id : createNodeId();
181
191
  const linkNode = {
182
192
  id: nodeId,
183
- origin: existing.found ? existing.node.origin : { kind: "none" },
193
+ origin: writeTarget.existing !== null ? writeTarget.existing.node.origin : { kind: "none" },
184
194
  state: {
185
195
  kind: "symlink",
186
196
  mode: "120000",
@@ -188,118 +198,61 @@ function openVirtualWorkdirSession(source, state) {
188
198
  }
189
199
  };
190
200
  state.setNode(linkNode);
191
- updateParentOverlay(state, parentNode.id, overlayBindEntry(parentNode.state.overlay, name, nodeId));
192
- state.appendChange({
193
- op: isNew ? "add" : "modify",
194
- path
195
- });
201
+ if (writeTarget.existing === null || writeTarget.parentNode.state.overlay.addedEntries.has(writeTarget.name)) updateParentOverlay(state, writeTarget.parentNode.id, overlayBindEntry(writeTarget.parentNode.state.overlay, writeTarget.name, nodeId));
196
202
  });
197
203
  },
198
204
  delete(path) {
199
205
  runInWriteTransaction(state, () => {
200
- assertValidVirtualPath(path);
201
- const parent = parentPath(path);
202
- const name = baseName(path);
203
- const parentResolved = parent !== null ? resolvePath(source, state, parent) : {
204
- found: true,
205
- node: getRootNode(state)
206
- };
207
- if (!parentResolved.found || parentResolved.node === null) throw new VirtualPathNotFoundError(parent ?? path);
208
- const parentNode = parentResolved.node;
209
- if (parentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(parent ?? path);
210
- const existing = resolveChild(source, state, parentNode, parent ?? "", name);
211
- if (!existing.found || existing.node === null) throw new VirtualPathNotFoundError(path);
212
- updateParentOverlay(state, parentNode.id, overlayTombstoneEntry(parentNode.state.overlay, name));
213
- state.appendChange({
214
- op: "delete",
215
- path
216
- });
206
+ changeIndexPlanner.apply(changeIndexPlanner.planRefreshForPath(path, { treatMissingAsIncremental: true }));
207
+ dirtyDirPlanner.rebuild([path]);
208
+ }, invalidateDiffCaches, () => {
209
+ const target = requireExistingWriteTarget(source, state, path);
210
+ updateParentOverlay(state, target.parentNode.id, overlayTombstoneEntry(target.parentNode.state.overlay, target.name));
217
211
  });
218
212
  },
219
213
  rename(from, to) {
220
214
  runInWriteTransaction(state, () => {
215
+ changeIndexPlanner.apply(changeIndexPlanner.planRewriteForRename(from, to));
216
+ dirtyDirPlanner.rebuild([from, to]);
217
+ }, invalidateDiffCaches, () => {
221
218
  assertValidVirtualPath(from);
222
219
  assertValidVirtualPath(to);
223
220
  if (from === to) return;
224
- const fromParent = parentPath(from);
225
- const fromName = baseName(from);
226
- const fromParentResolved = fromParent !== null ? resolvePath(source, state, fromParent) : {
227
- found: true,
228
- node: getRootNode(state)
229
- };
230
- if (!fromParentResolved.found || fromParentResolved.node === null) throw new VirtualPathNotFoundError(from);
231
- const fromParentNode = fromParentResolved.node;
232
- if (fromParentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(from);
233
- const fromChild = resolveChild(source, state, fromParentNode, fromParent ?? "", fromName);
234
- if (!fromChild.found || fromChild.node === null) throw new VirtualPathNotFoundError(from);
235
- const sourceNode = fromChild.node;
236
- const toParent = parentPath(to);
237
- const toName = baseName(to);
238
- const toParentResolved = toParent !== null ? resolvePath(source, state, toParent) : {
239
- found: true,
240
- node: getRootNode(state)
241
- };
242
- if (!toParentResolved.found || toParentResolved.node === null) throw new VirtualPathNotFoundError(to);
243
- const toParentNode = toParentResolved.node;
244
- if (toParentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(to);
245
- const toExisting = resolveChild(source, state, toParentNode, toParent ?? "", toName);
246
- if (toExisting.found && toExisting.node !== null) throw new VirtualPathAlreadyExistsError(to);
221
+ const { from: fromTarget, to: toTarget } = resolveWriteTransfer(source, state, from, to);
222
+ const sourceNode = fromTarget.existing.node;
223
+ if (toTarget.existing !== null) throw new VirtualPathAlreadyExistsError(to);
247
224
  if (sourceNode.state.kind === "directory") {
248
225
  const toPath = to;
249
226
  const fromPath = from;
250
227
  if (toPath.startsWith(fromPath + "/") || toPath === fromPath) throw new Error(`Cannot rename '${from}' to '${to}': destination is a subdirectory of source`);
251
228
  }
252
- if (fromParentNode.id === toParentNode.id) updateParentOverlay(state, fromParentNode.id, overlayRenameEntry(fromParentNode.state.overlay, fromName, toName, sourceNode.id));
229
+ if (fromTarget.parentNode.id === toTarget.parentNode.id) updateParentOverlay(state, fromTarget.parentNode.id, overlayRenameEntry(fromTarget.parentNode.state.overlay, fromTarget.name, toTarget.name, sourceNode.id));
253
230
  else {
254
- updateParentOverlay(state, fromParentNode.id, overlayTombstoneEntry(fromParentNode.state.overlay, fromName));
255
- updateParentOverlay(state, toParentNode.id, overlayBindEntry(toParentNode.state.overlay, toName, sourceNode.id));
231
+ updateParentOverlay(state, fromTarget.parentNode.id, overlayTombstoneEntry(fromTarget.parentNode.state.overlay, fromTarget.name));
232
+ updateParentOverlay(state, toTarget.parentNode.id, overlayBindEntry(toTarget.parentNode.state.overlay, toTarget.name, sourceNode.id));
256
233
  }
257
- state.appendChange({
258
- op: "rename",
259
- from,
260
- to
261
- });
262
234
  });
263
235
  },
264
236
  copy(from, to) {
265
237
  runInWriteTransaction(state, () => {
238
+ changeIndexPlanner.apply(changeIndexPlanner.planWriteForCopy(from, to));
239
+ dirtyDirPlanner.rebuild([from, to]);
240
+ }, invalidateDiffCaches, () => {
266
241
  assertValidVirtualPath(from);
267
242
  assertValidVirtualPath(to);
268
243
  if (from === to) throw new VirtualPathAlreadyExistsError(to);
269
- const fromParent = parentPath(from);
270
- const fromName = baseName(from);
271
- const fromParentResolved = fromParent !== null ? resolvePath(source, state, fromParent) : {
272
- found: true,
273
- node: getRootNode(state)
274
- };
275
- if (!fromParentResolved.found || fromParentResolved.node === null) throw new VirtualPathNotFoundError(from);
276
- const fromParentNode = fromParentResolved.node;
277
- if (fromParentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(from);
278
- const fromChild = resolveChild(source, state, fromParentNode, fromParent ?? "", fromName);
279
- if (!fromChild.found || fromChild.node === null) throw new VirtualPathNotFoundError(from);
280
- const sourceNode = fromChild.node;
281
- const toParent = parentPath(to);
282
- const toName = baseName(to);
283
- const toParentResolved = toParent !== null ? resolvePath(source, state, toParent) : {
284
- found: true,
285
- node: getRootNode(state)
286
- };
287
- if (!toParentResolved.found || toParentResolved.node === null) throw new VirtualPathNotFoundError(to);
288
- const toParentNode = toParentResolved.node;
289
- if (toParentNode.state.kind !== "directory") throw new VirtualNotDirectoryError(to);
290
- const toExisting = resolveChild(source, state, toParentNode, toParent ?? "", toName);
291
- if (toExisting.found && toExisting.node !== null) throw new VirtualPathAlreadyExistsError(to);
244
+ const { from: fromTarget, to: toTarget } = resolveWriteTransfer(source, state, from, to);
245
+ const sourceNode = fromTarget.existing.node;
246
+ if (toTarget.existing !== null) throw new VirtualPathAlreadyExistsError(to);
292
247
  const newNodeId = cloneNodeGraphForCopy(source, state, sourceNode, from);
293
- updateParentOverlay(state, toParentNode.id, overlayBindEntry(toParentNode.state.overlay, toName, newNodeId));
294
- state.appendChange({
295
- op: "copy",
296
- from,
297
- to
298
- });
248
+ updateParentOverlay(state, toTarget.parentNode.id, overlayBindEntry(toTarget.parentNode.state.overlay, toTarget.name, newNodeId));
299
249
  });
300
250
  },
301
251
  revert(path) {
302
252
  runInWriteTransaction(state, () => {
253
+ changeIndexPlanner.apply(changeIndexPlanner.planRefreshForPath(path));
254
+ dirtyDirPlanner.rebuild([path]);
255
+ }, invalidateDiffCaches, () => {
303
256
  assertValidVirtualPath(path);
304
257
  const resolved = resolvePath(source, state, path);
305
258
  if (!resolved.found || resolved.node === null) throw new VirtualPathNotFoundError(path);
@@ -307,101 +260,21 @@ function openVirtualWorkdirSession(source, state) {
307
260
  const reverted = revertNodeState(node);
308
261
  if (reverted === node) throw new VirtualRevertNotSupportedError(path);
309
262
  state.setNode(reverted);
310
- state.appendChange({
311
- op: "revert",
312
- path
313
- });
314
263
  });
315
264
  },
265
+ diff() {
266
+ return computeVirtualDiff(state);
267
+ },
316
268
  writeTree() {
317
269
  return writeTreeFromSession(source, state);
318
270
  },
319
271
  reset(baseTree) {
320
- runInWriteTransaction(state, () => {
272
+ runInWriteTransaction(state, null, invalidateDiffCaches, () => {
321
273
  state.reset(baseTree);
274
+ dirtyDirPlanner.clear();
322
275
  });
323
- },
324
- listChanges() {
325
- return mapInternalChangesToVirtualChanges(state.listChangeRecords());
326
- }
327
- };
328
- }
329
- function getRootNode(state) {
330
- const root = state.getNode(VIRTUAL_ROOT_NODE_ID);
331
- if (root === null) throw new Error("Virtual workdir session is missing root node");
332
- return root;
333
- }
334
- function runInWriteTransaction(state, fn) {
335
- return state.transact(fn);
336
- }
337
- /**
338
- * 更新父节点的 overlay(创建新节点对象替代 Map 中的旧引用)
339
- */
340
- function updateParentOverlay(state, parentId, newOverlay) {
341
- const parentNode = state.getNode(parentId);
342
- if (parentNode === null || parentNode.state.kind !== "directory") throw new Error("updateParentOverlay: parent is not a directory");
343
- state.setNode({
344
- ...parentNode,
345
- state: {
346
- ...parentNode.state,
347
- overlay: newOverlay
348
276
  }
349
- });
350
- }
351
- function statNode(source, node, path) {
352
- if (node.state.kind === "directory") return statDirectoryNode(node);
353
- if (node.state.kind === "symlink") {
354
- const hash = node.origin.kind === "repo-blob" ? node.origin.hash : null;
355
- let size = 0;
356
- if (node.state.target !== void 0) size = node.state.target.length;
357
- else if (hash !== null) size = readRepoBlobContent(source, hash, path).length;
358
- return {
359
- kind: "symlink",
360
- mode: "120000",
361
- size,
362
- hash
363
- };
364
- }
365
- const hash = node.origin.kind === "repo-blob" ? node.origin.hash : null;
366
- let size = 0;
367
- if (node.state.content !== void 0) size = node.state.content.length;
368
- else if (hash !== null) size = readRepoBlobContent(source, hash, path).length;
369
- return {
370
- kind: "blob",
371
- mode: node.state.mode,
372
- size,
373
- hash
374
277
  };
375
278
  }
376
- function statDirectoryNode(node) {
377
- return {
378
- kind: "tree",
379
- mode: "40000",
380
- size: 0,
381
- hash: node.origin.kind === "repo-tree" ? node.origin.hash : null
382
- };
383
- }
384
- function cloneNodeGraphForCopy(source, state, node, path) {
385
- const newNodeId = createNodeId();
386
- const cloned = cloneSessionNodeForCopy(node, newNodeId);
387
- state.setNode(cloned);
388
- if (node.state.kind !== "directory" || cloned.state.kind !== "directory") return newNodeId;
389
- let overlay = cloned.state.overlay;
390
- const children = listDirectoryChildren(source, state, node, path);
391
- for (const child of children) {
392
- const childNode = state.getNode(child.nodeId);
393
- if (childNode === null) continue;
394
- const clonedChildId = cloneNodeGraphForCopy(source, state, childNode, path === "" ? child.name : `${path}/${child.name}`);
395
- overlay = overlayBindEntry(overlay, child.name, clonedChildId);
396
- }
397
- state.setNode({
398
- ...cloned,
399
- state: {
400
- kind: "directory",
401
- overlay
402
- }
403
- });
404
- return newNodeId;
405
- }
406
279
  //#endregion
407
280
  export { createVirtualWorkdirSession, openVirtualWorkdirSession };