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.
- package/LICENSE +21 -0
- package/README.md +971 -0
- package/dist/index.cjs +12609 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.mjs +12600 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +90 -0
- package/src/api/repository.js +6885 -0
- package/src/backend/isomorphic-git-backend.js +687 -0
- package/src/backend/lazy-git-backend.js +180 -0
- package/src/browser/helpers.js +189 -0
- package/src/browser/index.d.ts +174 -0
- package/src/browser/index.js +12 -0
- package/src/core/background-ops.js +272 -0
- package/src/core/bookmark-store.js +232 -0
- package/src/core/change-graph.js +285 -0
- package/src/core/conflict-model.js +505 -0
- package/src/core/jj-checkout.js +78 -0
- package/src/core/jj-operation-store.js +120 -0
- package/src/core/jj-tree-state.js +94 -0
- package/src/core/jj-view-store.js +105 -0
- package/src/core/merge-driver-registry.js +432 -0
- package/src/core/operation-log.js +272 -0
- package/src/core/revset-engine.js +1081 -0
- package/src/core/storage-manager.js +207 -0
- package/src/core/tag-store.js +167 -0
- package/src/core/user-config.js +142 -0
- package/src/core/working-copy.js +314 -0
- package/src/core/workspace-manager.js +283 -0
- package/src/drivers/built-in-drivers.js +296 -0
- package/src/index.js +15 -0
- package/src/protos/local_working_copy.proto +76 -0
- package/src/protos/simple_op_store.proto +147 -0
- package/src/types.d.ts +1129 -0
- package/src/utils/errors.js +21 -0
- package/src/utils/id-generation.js +54 -0
- 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
|
+
}
|