isomorphic-jj 1.0.0

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 (37) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +971 -0
  3. package/dist/index.cjs +12609 -0
  4. package/dist/index.cjs.map +1 -0
  5. package/dist/index.mjs +12600 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/package.json +90 -0
  8. package/src/api/repository.js +6885 -0
  9. package/src/backend/isomorphic-git-backend.js +687 -0
  10. package/src/backend/lazy-git-backend.js +180 -0
  11. package/src/browser/helpers.js +189 -0
  12. package/src/browser/index.d.ts +174 -0
  13. package/src/browser/index.js +12 -0
  14. package/src/core/background-ops.js +272 -0
  15. package/src/core/bookmark-store.js +232 -0
  16. package/src/core/change-graph.js +285 -0
  17. package/src/core/conflict-model.js +505 -0
  18. package/src/core/jj-checkout.js +78 -0
  19. package/src/core/jj-operation-store.js +120 -0
  20. package/src/core/jj-tree-state.js +94 -0
  21. package/src/core/jj-view-store.js +105 -0
  22. package/src/core/merge-driver-registry.js +432 -0
  23. package/src/core/operation-log.js +272 -0
  24. package/src/core/revset-engine.js +1081 -0
  25. package/src/core/storage-manager.js +207 -0
  26. package/src/core/tag-store.js +167 -0
  27. package/src/core/user-config.js +142 -0
  28. package/src/core/working-copy.js +314 -0
  29. package/src/core/workspace-manager.js +283 -0
  30. package/src/drivers/built-in-drivers.js +296 -0
  31. package/src/index.js +15 -0
  32. package/src/protos/local_working_copy.proto +76 -0
  33. package/src/protos/simple_op_store.proto +147 -0
  34. package/src/types.d.ts +1129 -0
  35. package/src/utils/errors.js +21 -0
  36. package/src/utils/id-generation.js +54 -0
  37. package/src/utils/validation.js +124 -0
@@ -0,0 +1,272 @@
1
+ /**
2
+ * OperationLog - Manages the append-only operation log
3
+ *
4
+ * Records all repository mutations for complete undo/redo and time-travel.
5
+ */
6
+
7
+ import { JJError } from '../utils/errors.js';
8
+ import { generateOperationId } from '../utils/id-generation.js';
9
+
10
+ export class OperationLog {
11
+ /**
12
+ * @param {Storage} storage - Storage manager instance
13
+ */
14
+ constructor(storage) {
15
+ this.storage = storage;
16
+ this.operations = [];
17
+ this.headOperationId = null;
18
+ }
19
+
20
+ /**
21
+ * Initialize empty operation log
22
+ */
23
+ async init() {
24
+ this.operations = [];
25
+ this.headOperationId = null;
26
+
27
+ await this.storage.write('repo/op_log/oplog.jsonl', '');
28
+ }
29
+
30
+ /**
31
+ * Load operation log from storage
32
+ */
33
+ async load() {
34
+ const lines = await this.storage.readLines('repo/op_log/oplog.jsonl');
35
+
36
+ this.operations = lines;
37
+ this.headOperationId = lines.length > 0 ? lines[lines.length - 1].id : null;
38
+ }
39
+
40
+ /**
41
+ * Record a new operation
42
+ *
43
+ * @param {Object} operation - Operation to record (without id)
44
+ * @returns {Promise<Object>} Recorded operation with id
45
+ */
46
+ async recordOperation(operation) {
47
+ // Ensure operations are loaded
48
+ if (this.operations.length === 0 && this.headOperationId === null) {
49
+ await this.load();
50
+ }
51
+
52
+ // Set parent to current head
53
+ if (this.headOperationId) {
54
+ operation.parents = [this.headOperationId];
55
+ } else {
56
+ operation.parents = operation.parents || [];
57
+ }
58
+
59
+ // Generate operation ID
60
+ const id = await generateOperationId(operation);
61
+ const fullOperation = {
62
+ id,
63
+ ...operation,
64
+ };
65
+
66
+ // Append to log
67
+ const line = JSON.stringify(fullOperation);
68
+ await this.storage.appendLine('repo/op_log/oplog.jsonl', line);
69
+
70
+ // Update in-memory state
71
+ this.operations.push(fullOperation);
72
+ this.headOperationId = id;
73
+
74
+ return fullOperation;
75
+ }
76
+
77
+ /**
78
+ * Get all operations
79
+ *
80
+ * @returns {Promise<Array>} Array of all operations (chronological order)
81
+ */
82
+ async list() {
83
+ if (this.operations.length === 0 && this.headOperationId === null) {
84
+ await this.load();
85
+ }
86
+
87
+ return this.operations;
88
+ }
89
+
90
+ /**
91
+ * Get the head (latest) operation
92
+ *
93
+ * @returns {Promise<Object|null>} Latest operation or null if log is empty
94
+ */
95
+ async getHeadOperation() {
96
+ if (this.operations.length === 0 && this.headOperationId === null) {
97
+ await this.load();
98
+ }
99
+
100
+ return this.operations.length > 0 ? this.operations[this.operations.length - 1] : null;
101
+ }
102
+
103
+ /**
104
+ * Undo the last operation
105
+ *
106
+ * Returns the view from the previous operation, allowing the caller
107
+ * to restore repository state.
108
+ *
109
+ * @returns {Promise<Object>} View from previous operation
110
+ */
111
+ async undo() {
112
+ if (this.operations.length === 0 && this.headOperationId === null) {
113
+ await this.load();
114
+ }
115
+
116
+ if (this.operations.length === 0) {
117
+ throw new JJError('NO_OPERATIONS_TO_UNDO', 'No operations to undo', {
118
+ suggestion: 'Cannot undo when operation log is empty',
119
+ });
120
+ }
121
+
122
+ // Get the previous operation (before current head)
123
+ if (this.operations.length === 1) {
124
+ // Special case: undoing the first operation returns its view
125
+ return this.operations[0].view;
126
+ }
127
+
128
+ // Return view from parent operation
129
+ const currentHead = this.operations[this.operations.length - 1];
130
+ const parentId = currentHead.parents[0];
131
+
132
+ // Find parent operation
133
+ const parentOp = this.operations.find(op => op.id === parentId);
134
+
135
+ if (!parentOp) {
136
+ // If parent not found, return view from operation before current
137
+ return this.operations[this.operations.length - 2].view;
138
+ }
139
+
140
+ return parentOp.view;
141
+ }
142
+
143
+ /**
144
+ * Get snapshot (view) at a specific operation
145
+ *
146
+ * @param {string} operationId - Operation ID
147
+ * @returns {Promise<Object>} View from that operation
148
+ */
149
+ async getSnapshotAt(operationId) {
150
+ if (this.operations.length === 0 && this.headOperationId === null) {
151
+ await this.load();
152
+ }
153
+
154
+ const operation = this.operations.find(op => op.id === operationId);
155
+
156
+ if (!operation) {
157
+ throw new JJError('OPERATION_NOT_FOUND', `Operation ${operationId} not found`, {
158
+ operationId,
159
+ suggestion: 'Check operation ID or use list() to see available operations',
160
+ });
161
+ }
162
+
163
+ return operation.view;
164
+ }
165
+
166
+ /**
167
+ * Get operation by ID
168
+ *
169
+ * @param {string} operationId - Operation ID
170
+ * @returns {Promise<Object|null>} Operation or null if not found
171
+ */
172
+ async getOperation(operationId) {
173
+ if (this.operations.length === 0 && this.headOperationId === null) {
174
+ await this.load();
175
+ }
176
+
177
+ return this.operations.find(op => op.id === operationId) || null;
178
+ }
179
+
180
+ /**
181
+ * Abandon an operation (remove from log)
182
+ *
183
+ * This is an advanced operation that removes an operation from the log.
184
+ * Children of the abandoned operation are relinked to the abandoned operation's parent.
185
+ *
186
+ * @param {string} operationId - Operation ID to abandon
187
+ * @returns {Promise<Object>} Result with abandoned operation and relinked children
188
+ */
189
+ async abandon(operationId) {
190
+ if (this.operations.length === 0 && this.headOperationId === null) {
191
+ await this.load();
192
+ }
193
+
194
+ // Validate operationId
195
+ if (!operationId) {
196
+ throw new JJError('INVALID_ARGUMENT', 'Missing operation ID', {
197
+ suggestion: 'Provide an operation ID to abandon',
198
+ });
199
+ }
200
+
201
+ // Find the operation
202
+ const opIndex = this.operations.findIndex(op => op.id === operationId);
203
+ if (opIndex === -1) {
204
+ throw new JJError('OPERATION_NOT_FOUND', `Operation ${operationId} not found`, {
205
+ operationId,
206
+ suggestion: 'Use operations.list() to see available operations',
207
+ });
208
+ }
209
+
210
+ const operation = this.operations[opIndex];
211
+
212
+ // Cannot abandon the only operation
213
+ if (this.operations.length === 1) {
214
+ throw new JJError('CANNOT_ABANDON', 'Cannot abandon the only operation in the log', {
215
+ suggestion: 'The operation log must contain at least one operation',
216
+ });
217
+ }
218
+
219
+ // Find children (operations that reference this as a parent)
220
+ const children = this.operations.filter(op =>
221
+ op.parents && op.parents.includes(operationId)
222
+ );
223
+
224
+ // Get the parent of the operation being abandoned (may be undefined for root)
225
+ const grandparentId = operation.parents && operation.parents.length > 0
226
+ ? operation.parents[0]
227
+ : undefined;
228
+
229
+ // Relink children to grandparent
230
+ const relinkedChildren = [];
231
+ for (const child of children) {
232
+ // Replace references to abandoned operation with grandparent
233
+ const oldParents = [...child.parents];
234
+ child.parents = child.parents.map(p =>
235
+ p === operationId ? grandparentId : p
236
+ ).filter(Boolean); // Remove undefined if there was no grandparent
237
+
238
+ // If there was no grandparent, the child becomes a root operation
239
+ if (!grandparentId && child.parents.length === 0) {
240
+ child.parents = [];
241
+ }
242
+
243
+ relinkedChildren.push({
244
+ operationId: child.id,
245
+ oldParents,
246
+ newParents: child.parents,
247
+ });
248
+ }
249
+
250
+ // Remove from array
251
+ this.operations.splice(opIndex, 1);
252
+
253
+ // Update head if we abandoned the head operation
254
+ if (this.headOperationId === operationId) {
255
+ this.headOperationId = this.operations.length > 0
256
+ ? this.operations[this.operations.length - 1].id
257
+ : null;
258
+ }
259
+
260
+ // Rewrite JSONL file
261
+ const content = this.operations
262
+ .map(op => JSON.stringify(op))
263
+ .join('\n') + (this.operations.length > 0 ? '\n' : '');
264
+ await this.storage.write('repo/op_log/oplog.jsonl', content);
265
+
266
+ return {
267
+ abandoned: operation,
268
+ relinkedChildren,
269
+ newHead: this.headOperationId,
270
+ };
271
+ }
272
+ }