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,505 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConflictModel - First-class conflict handling
|
|
3
|
+
*
|
|
4
|
+
* In JJ, conflicts are not errors but structured data that can be:
|
|
5
|
+
* - Stored in commits
|
|
6
|
+
* - Rebased along with changes
|
|
7
|
+
* - Resolved incrementally
|
|
8
|
+
* - Committed and shared
|
|
9
|
+
*
|
|
10
|
+
* This matches JJ's philosophy: conflicts never block you.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import path from 'path';
|
|
14
|
+
import { JJError } from '../utils/errors.js';
|
|
15
|
+
import { generateId } from '../utils/id-generation.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Conflict types supported by JJ
|
|
19
|
+
*/
|
|
20
|
+
export const ConflictType = {
|
|
21
|
+
CONTENT: 'content', // File content conflicts (3-way merge)
|
|
22
|
+
PATH: 'path', // Path/rename conflicts
|
|
23
|
+
DELETE_MODIFY: 'delete-modify', // File deleted in one side, modified in other
|
|
24
|
+
MODIFY_DELETE: 'modify-delete', // File modified in one side, deleted in other
|
|
25
|
+
ADD_ADD: 'add-add', // Same path added in both sides
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* ConflictModel manages conflict detection, storage, and resolution
|
|
30
|
+
*/
|
|
31
|
+
export class ConflictModel {
|
|
32
|
+
constructor(storage, fs, mergeDriverRegistry = null) {
|
|
33
|
+
this.storage = storage;
|
|
34
|
+
this.fs = fs;
|
|
35
|
+
this.mergeDriverRegistry = mergeDriverRegistry; // v0.5: merge drivers
|
|
36
|
+
this.conflicts = new Map(); // conflictId -> Conflict
|
|
37
|
+
this.fileConflicts = new Map(); // path -> conflictId
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Initialize conflict storage
|
|
42
|
+
*/
|
|
43
|
+
async init() {
|
|
44
|
+
try {
|
|
45
|
+
const data = await this.storage.read('repo/conflicts/conflicts.json');
|
|
46
|
+
if (data) {
|
|
47
|
+
// storage.read() already parses JSON
|
|
48
|
+
this.conflicts = new Map(Object.entries(data.conflicts || {}));
|
|
49
|
+
this.fileConflicts = new Map(Object.entries(data.fileConflicts || {}));
|
|
50
|
+
}
|
|
51
|
+
} catch (error) {
|
|
52
|
+
// No conflicts file yet, that's fine
|
|
53
|
+
this.conflicts = new Map();
|
|
54
|
+
this.fileConflicts = new Map();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Load conflicts from storage
|
|
60
|
+
*/
|
|
61
|
+
async load() {
|
|
62
|
+
await this.init();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Save conflicts to storage
|
|
67
|
+
*/
|
|
68
|
+
async save() {
|
|
69
|
+
const data = {
|
|
70
|
+
conflicts: Object.fromEntries(this.conflicts),
|
|
71
|
+
fileConflicts: Object.fromEntries(this.fileConflicts),
|
|
72
|
+
};
|
|
73
|
+
// storage.write() will handle JSON stringification
|
|
74
|
+
await this.storage.write('repo/conflicts/conflicts.json', data);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Detect conflicts during merge/rebase operations
|
|
79
|
+
*
|
|
80
|
+
* @param {Object} opts - Merge options
|
|
81
|
+
* @param {string} opts.baseChange - Common ancestor change
|
|
82
|
+
* @param {string} opts.leftChange - Left side change
|
|
83
|
+
* @param {string} opts.rightChange - Right side change
|
|
84
|
+
* @param {Map} opts.baseFiles - Files in base
|
|
85
|
+
* @param {Map} opts.leftFiles - Files in left
|
|
86
|
+
* @param {Map} opts.rightFiles - Files in right
|
|
87
|
+
* @param {Object} opts.drivers - Custom merge drivers (v0.5)
|
|
88
|
+
* @param {string} opts.workingCopyDir - Working copy directory for file writes (v0.5)
|
|
89
|
+
* @returns {Array<Conflict>} Detected conflicts
|
|
90
|
+
*/
|
|
91
|
+
async detectConflicts(opts) {
|
|
92
|
+
const { baseFiles, leftFiles, rightFiles, drivers = {}, workingCopyDir, baseChange, leftChange, rightChange } = opts;
|
|
93
|
+
const conflicts = [];
|
|
94
|
+
|
|
95
|
+
// Get all unique paths across all versions
|
|
96
|
+
const allPaths = new Set([
|
|
97
|
+
...baseFiles.keys(),
|
|
98
|
+
...leftFiles.keys(),
|
|
99
|
+
...rightFiles.keys(),
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
for (const path of allPaths) {
|
|
103
|
+
const baseContent = baseFiles.get(path);
|
|
104
|
+
const leftContent = leftFiles.get(path);
|
|
105
|
+
const rightContent = rightFiles.get(path);
|
|
106
|
+
|
|
107
|
+
// v0.5: Try merge driver first if available
|
|
108
|
+
if (this.mergeDriverRegistry) {
|
|
109
|
+
const mergeResult = await this._tryMergeDriver(
|
|
110
|
+
path,
|
|
111
|
+
{base: baseContent, ours: leftContent, theirs: rightContent},
|
|
112
|
+
drivers,
|
|
113
|
+
{ baseChange, leftChange, rightChange }
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
if (mergeResult) {
|
|
117
|
+
// Driver handled it
|
|
118
|
+
if (mergeResult.hasConflict) {
|
|
119
|
+
// Driver produced a conflict (possibly partial merge)
|
|
120
|
+
// Use conflict info from driver result if available
|
|
121
|
+
if (mergeResult.conflicts && mergeResult.conflicts.length > 0) {
|
|
122
|
+
// Driver provided detailed conflict info
|
|
123
|
+
for (const driverConflict of mergeResult.conflicts) {
|
|
124
|
+
const conflict = this._createConflict({
|
|
125
|
+
type: driverConflict.type || 'driver-conflict',
|
|
126
|
+
path,
|
|
127
|
+
sides: { base: baseContent, left: leftContent, right: rightContent },
|
|
128
|
+
message: driverConflict.message || mergeResult.message || `Merge driver detected conflicts`,
|
|
129
|
+
driverResult: mergeResult,
|
|
130
|
+
driverFailed: mergeResult.driverFailed,
|
|
131
|
+
driverError: mergeResult.driverError,
|
|
132
|
+
});
|
|
133
|
+
conflicts.push(conflict);
|
|
134
|
+
}
|
|
135
|
+
} else {
|
|
136
|
+
// Driver didn't provide detailed conflicts, create generic one
|
|
137
|
+
const conflict = this._createConflict({
|
|
138
|
+
type: 'driver-conflict',
|
|
139
|
+
path,
|
|
140
|
+
sides: { base: baseContent, left: leftContent, right: rightContent },
|
|
141
|
+
message: mergeResult.message || `Merge driver detected conflicts`,
|
|
142
|
+
driverResult: mergeResult,
|
|
143
|
+
driverFailed: mergeResult.driverFailed,
|
|
144
|
+
driverError: mergeResult.driverError,
|
|
145
|
+
});
|
|
146
|
+
conflicts.push(conflict);
|
|
147
|
+
}
|
|
148
|
+
} else {
|
|
149
|
+
// Driver successfully merged - write result to working copy
|
|
150
|
+
if (workingCopyDir && mergeResult.content !== null) {
|
|
151
|
+
await this._writeDriverResult(workingCopyDir, path, mergeResult);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
continue; // Driver handled this file, skip default detection
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Fall back to default conflict detection
|
|
159
|
+
const conflict = this._detectPathConflict(path, baseContent, leftContent, rightContent);
|
|
160
|
+
if (conflict) {
|
|
161
|
+
conflicts.push(conflict);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return conflicts;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Detect conflict for a single path
|
|
170
|
+
*/
|
|
171
|
+
_detectPathConflict(path, base, left, right) {
|
|
172
|
+
const baseExists = base !== undefined;
|
|
173
|
+
const leftExists = left !== undefined;
|
|
174
|
+
const rightExists = right !== undefined;
|
|
175
|
+
|
|
176
|
+
// No conflict if all three are the same
|
|
177
|
+
if (base === left && left === right) {
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// No conflict if only one side changed
|
|
182
|
+
if (base === left && base !== right) {
|
|
183
|
+
return null; // Right side wins
|
|
184
|
+
}
|
|
185
|
+
if (base === right && base !== left) {
|
|
186
|
+
return null; // Left side wins
|
|
187
|
+
}
|
|
188
|
+
if (base !== left && base !== right && left === right) {
|
|
189
|
+
return null; // Both sides made the same change
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Now we have actual conflicts
|
|
193
|
+
if (!baseExists && leftExists && rightExists) {
|
|
194
|
+
// File added in both sides
|
|
195
|
+
if (left === right) {
|
|
196
|
+
return null; // Same content, no conflict
|
|
197
|
+
}
|
|
198
|
+
return this._createConflict({
|
|
199
|
+
type: ConflictType.ADD_ADD,
|
|
200
|
+
path,
|
|
201
|
+
sides: { left, right },
|
|
202
|
+
message: `File added with different content in both sides`,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
if (baseExists && !leftExists && !rightExists) {
|
|
207
|
+
// File deleted in both sides
|
|
208
|
+
return null; // No conflict
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
if (baseExists && !leftExists && rightExists) {
|
|
212
|
+
// Deleted on left, modified on right
|
|
213
|
+
return this._createConflict({
|
|
214
|
+
type: ConflictType.DELETE_MODIFY,
|
|
215
|
+
path,
|
|
216
|
+
sides: { base, right },
|
|
217
|
+
message: `File deleted on one side but modified on the other`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (baseExists && leftExists && !rightExists) {
|
|
222
|
+
// Modified on left, deleted on right
|
|
223
|
+
return this._createConflict({
|
|
224
|
+
type: ConflictType.MODIFY_DELETE,
|
|
225
|
+
path,
|
|
226
|
+
sides: { base, left },
|
|
227
|
+
message: `File modified on one side but deleted on the other`,
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (baseExists && leftExists && rightExists) {
|
|
232
|
+
// Content conflict - all three versions exist but differ
|
|
233
|
+
return this._createConflict({
|
|
234
|
+
type: ConflictType.CONTENT,
|
|
235
|
+
path,
|
|
236
|
+
sides: { base, left, right },
|
|
237
|
+
message: `Conflicting changes to file content`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return null;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Create a conflict object
|
|
246
|
+
*/
|
|
247
|
+
_createConflict({ type, path, sides, message, driverFailed, driverError }) {
|
|
248
|
+
const conflictId = generateId('conflict');
|
|
249
|
+
const conflict = {
|
|
250
|
+
conflictId,
|
|
251
|
+
type,
|
|
252
|
+
path,
|
|
253
|
+
sides,
|
|
254
|
+
message,
|
|
255
|
+
resolved: false,
|
|
256
|
+
timestamp: new Date().toISOString(),
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
// Add driver failure metadata if present
|
|
260
|
+
if (driverFailed !== undefined) {
|
|
261
|
+
conflict.driverFailed = driverFailed;
|
|
262
|
+
}
|
|
263
|
+
if (driverError !== undefined) {
|
|
264
|
+
conflict.driverError = driverError;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return conflict;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Store a conflict
|
|
272
|
+
*/
|
|
273
|
+
async addConflict(conflict) {
|
|
274
|
+
this.conflicts.set(conflict.conflictId, conflict);
|
|
275
|
+
this.fileConflicts.set(conflict.path, conflict.conflictId);
|
|
276
|
+
await this.save();
|
|
277
|
+
return conflict;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Get conflict by ID
|
|
282
|
+
*/
|
|
283
|
+
getConflict(conflictId) {
|
|
284
|
+
return this.conflicts.get(conflictId);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Get conflict for a file path
|
|
289
|
+
*/
|
|
290
|
+
getConflictForPath(path) {
|
|
291
|
+
const conflictId = this.fileConflicts.get(path);
|
|
292
|
+
if (!conflictId) return null;
|
|
293
|
+
return this.conflicts.get(conflictId);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* List all conflicts
|
|
298
|
+
*/
|
|
299
|
+
listConflicts(opts = {}) {
|
|
300
|
+
let conflicts = Array.from(this.conflicts.values());
|
|
301
|
+
|
|
302
|
+
if (opts.resolved !== undefined) {
|
|
303
|
+
conflicts = conflicts.filter(c => c.resolved === opts.resolved);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (opts.type) {
|
|
307
|
+
conflicts = conflicts.filter(c => c.type === opts.type);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return conflicts;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Mark a conflict as resolved
|
|
315
|
+
*/
|
|
316
|
+
async resolveConflict(conflictId, resolution) {
|
|
317
|
+
const conflict = this.conflicts.get(conflictId);
|
|
318
|
+
if (!conflict) {
|
|
319
|
+
throw new JJError('CONFLICT_NOT_FOUND', `Conflict ${conflictId} not found`);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Support both object-based and legacy string resolutions
|
|
323
|
+
let resolvedContent;
|
|
324
|
+
|
|
325
|
+
if (typeof resolution === 'string') {
|
|
326
|
+
// Legacy string format: 'manual', 'ours', 'theirs', etc.
|
|
327
|
+
resolvedContent = { type: 'manual', value: resolution };
|
|
328
|
+
} else if (resolution && typeof resolution === 'object') {
|
|
329
|
+
// New object-based format
|
|
330
|
+
if (resolution.side) {
|
|
331
|
+
// { side: 'ours' | 'theirs' | 'base' }
|
|
332
|
+
if (!['ours', 'theirs', 'base'].includes(resolution.side)) {
|
|
333
|
+
throw new JJError('INVALID_RESOLUTION', `Invalid side: ${resolution.side}`, {
|
|
334
|
+
suggestion: 'Use "ours", "theirs", or "base"'
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
resolvedContent = { type: 'side', side: resolution.side };
|
|
338
|
+
} else if (resolution.content !== undefined) {
|
|
339
|
+
// { content: string | Uint8Array }
|
|
340
|
+
resolvedContent = { type: 'content', content: resolution.content };
|
|
341
|
+
} else if (resolution.hunks) {
|
|
342
|
+
// { hunks: [...] } for partial resolution
|
|
343
|
+
resolvedContent = { type: 'hunks', hunks: resolution.hunks };
|
|
344
|
+
} else {
|
|
345
|
+
// Store as-is for extensibility
|
|
346
|
+
resolvedContent = resolution;
|
|
347
|
+
}
|
|
348
|
+
} else {
|
|
349
|
+
throw new JJError('INVALID_RESOLUTION', 'Resolution must be a string or object');
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
conflict.resolved = true;
|
|
353
|
+
conflict.resolution = resolvedContent;
|
|
354
|
+
conflict.resolvedAt = new Date().toISOString();
|
|
355
|
+
|
|
356
|
+
await this.save();
|
|
357
|
+
return conflict;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Remove a conflict (after resolution is committed)
|
|
362
|
+
*/
|
|
363
|
+
async removeConflict(conflictId) {
|
|
364
|
+
const conflict = this.conflicts.get(conflictId);
|
|
365
|
+
if (!conflict) return;
|
|
366
|
+
|
|
367
|
+
this.conflicts.delete(conflictId);
|
|
368
|
+
this.fileConflicts.delete(conflict.path);
|
|
369
|
+
await this.save();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Check if a change has conflicts
|
|
374
|
+
*/
|
|
375
|
+
hasConflicts() {
|
|
376
|
+
return this.listConflicts({ resolved: false }).length > 0;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Create conflict markers for a content conflict (like Git's <<<<<<<, =======, >>>>>>>)
|
|
381
|
+
*/
|
|
382
|
+
generateConflictMarkers(conflict) {
|
|
383
|
+
if (conflict.type !== ConflictType.CONTENT) {
|
|
384
|
+
throw new JJError('INVALID_CONFLICT_TYPE',
|
|
385
|
+
'Can only generate markers for content conflicts');
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const { base, left, right } = conflict.sides;
|
|
389
|
+
|
|
390
|
+
return [
|
|
391
|
+
'<<<<<<< Left',
|
|
392
|
+
left || '',
|
|
393
|
+
'||||||| Base',
|
|
394
|
+
base || '',
|
|
395
|
+
'=======',
|
|
396
|
+
right || '',
|
|
397
|
+
'>>>>>>> Right',
|
|
398
|
+
].join('\n');
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Parse conflict markers from file content
|
|
403
|
+
*/
|
|
404
|
+
parseConflictMarkers(content) {
|
|
405
|
+
const markerRegex = /^<{7} Left\n([\s\S]*?)\n\|{7} Base\n([\s\S]*?)\n={7}\n([\s\S]*?)\n>{7} Right$/gm;
|
|
406
|
+
const matches = markerRegex.exec(content);
|
|
407
|
+
|
|
408
|
+
if (!matches) {
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
left: matches[1],
|
|
414
|
+
base: matches[2],
|
|
415
|
+
right: matches[3],
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Clear all conflicts
|
|
421
|
+
*/
|
|
422
|
+
async clear() {
|
|
423
|
+
this.conflicts.clear();
|
|
424
|
+
this.fileConflicts.clear();
|
|
425
|
+
await this.save();
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Try to use a merge driver for a file (v0.5)
|
|
430
|
+
*
|
|
431
|
+
* @param {string} path - File path
|
|
432
|
+
* @param {Object} content - Content versions { base, ours, theirs }
|
|
433
|
+
* @param {Object} customDrivers - Per-merge custom drivers
|
|
434
|
+
* @param {Object} metadata - Merge metadata
|
|
435
|
+
* @returns {Promise<Object|null>} Merge result or null if no driver
|
|
436
|
+
*/
|
|
437
|
+
async _tryMergeDriver(path, content, customDrivers, metadata) {
|
|
438
|
+
if (!this.mergeDriverRegistry) {
|
|
439
|
+
return null; // No registry available
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// Check if file is binary
|
|
443
|
+
const isBinary = this.mergeDriverRegistry.isBinaryFile(
|
|
444
|
+
path,
|
|
445
|
+
content.ours || content.theirs || content.base
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
// Find appropriate driver
|
|
449
|
+
const driver = this.mergeDriverRegistry.findDriver(path, customDrivers, isBinary);
|
|
450
|
+
|
|
451
|
+
if (!driver || driver === this.mergeDriverRegistry.defaultDriver) {
|
|
452
|
+
return null; // No custom driver found
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// Execute driver (errors are handled by wrapDriver)
|
|
456
|
+
// In non-strict mode: returns result with driverFailed metadata
|
|
457
|
+
// In strict mode: throws error
|
|
458
|
+
const result = await this.mergeDriverRegistry.executeDriver(
|
|
459
|
+
driver,
|
|
460
|
+
{ path, ...content, metadata },
|
|
461
|
+
isBinary
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Write merge driver result to working copy (v0.5)
|
|
469
|
+
*
|
|
470
|
+
* @param {string} workingCopyDir - Working copy directory
|
|
471
|
+
* @param {string} filePath - File path
|
|
472
|
+
* @param {Object} result - Merge result
|
|
473
|
+
*/
|
|
474
|
+
async _writeDriverResult(workingCopyDir, filePath, result) {
|
|
475
|
+
const fullPath = path.join(workingCopyDir, filePath);
|
|
476
|
+
|
|
477
|
+
// Ensure directory exists
|
|
478
|
+
const dir = path.dirname(fullPath);
|
|
479
|
+
await this.fs.promises.mkdir(dir, { recursive: true });
|
|
480
|
+
|
|
481
|
+
// Write main file
|
|
482
|
+
if (result.content !== null && result.content !== undefined) {
|
|
483
|
+
if (Buffer.isBuffer(result.content)) {
|
|
484
|
+
await this.fs.promises.writeFile(fullPath, result.content);
|
|
485
|
+
} else {
|
|
486
|
+
await this.fs.promises.writeFile(fullPath, result.content, 'utf-8');
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Write additional files if driver provided them
|
|
491
|
+
if (result.additionalFiles) {
|
|
492
|
+
for (const [additionalPath, additionalContent] of Object.entries(result.additionalFiles)) {
|
|
493
|
+
const additionalFullPath = path.join(workingCopyDir, additionalPath);
|
|
494
|
+
const additionalDir = path.dirname(additionalFullPath);
|
|
495
|
+
await this.fs.promises.mkdir(additionalDir, { recursive: true });
|
|
496
|
+
|
|
497
|
+
if (Buffer.isBuffer(additionalContent)) {
|
|
498
|
+
await this.fs.promises.writeFile(additionalFullPath, additionalContent);
|
|
499
|
+
} else {
|
|
500
|
+
await this.fs.promises.writeFile(additionalFullPath, additionalContent, 'utf-8');
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JJ Checkout File Manager
|
|
3
|
+
*
|
|
4
|
+
* Handles encoding/decoding of .jj/working_copy/checkout file using protobuf.
|
|
5
|
+
* This file tracks which operation the working copy is at.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import protobuf from 'protobufjs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
export class JJCheckout {
|
|
16
|
+
constructor(fs, dir) {
|
|
17
|
+
this.fs = fs;
|
|
18
|
+
this.dir = dir;
|
|
19
|
+
this.protoPath = path.join(__dirname, '..', 'protos', 'local_working_copy.proto');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Write checkout file
|
|
24
|
+
*
|
|
25
|
+
* @param {string} operationId - Operation ID as hex string (128 characters)
|
|
26
|
+
* @param {string} workspaceName - Workspace name (default: 'default')
|
|
27
|
+
*/
|
|
28
|
+
async writeCheckout(operationId, workspaceName = 'default') {
|
|
29
|
+
// Load protobuf schema
|
|
30
|
+
const root = await protobuf.load(this.protoPath);
|
|
31
|
+
const Checkout = root.lookupType('local_working_copy.Checkout');
|
|
32
|
+
|
|
33
|
+
// Convert hex operation ID to bytes
|
|
34
|
+
const opIdBuffer = Buffer.from(operationId, 'hex');
|
|
35
|
+
|
|
36
|
+
// Create message (use camelCase for protobufjs)
|
|
37
|
+
const message = Checkout.create({
|
|
38
|
+
operationId: opIdBuffer,
|
|
39
|
+
workspaceName: workspaceName
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// Verify the message
|
|
43
|
+
const errMsg = Checkout.verify(message);
|
|
44
|
+
if (errMsg) {
|
|
45
|
+
throw new Error(`Checkout message verification failed: ${errMsg}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Encode to binary
|
|
49
|
+
const buffer = Checkout.encode(message).finish();
|
|
50
|
+
|
|
51
|
+
// Write to .jj/working_copy/checkout
|
|
52
|
+
const checkoutPath = `${this.dir}/.jj/working_copy/checkout`;
|
|
53
|
+
await this.fs.promises.writeFile(checkoutPath, buffer);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Read checkout file
|
|
58
|
+
*
|
|
59
|
+
* @returns {Object} Decoded checkout data
|
|
60
|
+
*/
|
|
61
|
+
async readCheckout() {
|
|
62
|
+
const checkoutPath = `${this.dir}/.jj/working_copy/checkout`;
|
|
63
|
+
const buffer = await this.fs.promises.readFile(checkoutPath);
|
|
64
|
+
|
|
65
|
+
// Load protobuf schema
|
|
66
|
+
const root = await protobuf.load(this.protoPath);
|
|
67
|
+
const Checkout = root.lookupType('local_working_copy.Checkout');
|
|
68
|
+
|
|
69
|
+
// Decode
|
|
70
|
+
const message = Checkout.decode(buffer);
|
|
71
|
+
|
|
72
|
+
// Return the decoded message with proper field access
|
|
73
|
+
return {
|
|
74
|
+
operation_id: message.operationId,
|
|
75
|
+
workspace_name: message.workspaceName
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JJ Operation Store Manager
|
|
3
|
+
*
|
|
4
|
+
* Handles encoding/decoding of .jj/repo/op_store/operations/* files using protobuf.
|
|
5
|
+
* Operations track the history of changes to the repository.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import protobuf from 'protobufjs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
|
|
15
|
+
export class JJOperationStore {
|
|
16
|
+
constructor(fs, dir) {
|
|
17
|
+
this.fs = fs;
|
|
18
|
+
this.dir = dir;
|
|
19
|
+
this.protoPath = path.join(__dirname, '..', 'protos', 'simple_op_store.proto');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Write operation file
|
|
24
|
+
*
|
|
25
|
+
* @param {string} operationId - Operation ID as hex string (128 characters)
|
|
26
|
+
* @param {string} viewId - View ID as hex string (128 characters)
|
|
27
|
+
* @param {Array<string>} parentIds - Parent operation IDs as hex strings
|
|
28
|
+
* @param {Object} metadata - Operation metadata
|
|
29
|
+
*/
|
|
30
|
+
async writeOperation(operationId, viewId, parentIds, metadata) {
|
|
31
|
+
// Load protobuf schema
|
|
32
|
+
const root = await protobuf.load(this.protoPath);
|
|
33
|
+
const Operation = root.lookupType('simple_op_store.Operation');
|
|
34
|
+
|
|
35
|
+
// Convert hex IDs to bytes
|
|
36
|
+
const viewIdBuffer = Buffer.from(viewId, 'hex');
|
|
37
|
+
const parentBuffers = parentIds.map(id => Buffer.from(id, 'hex'));
|
|
38
|
+
|
|
39
|
+
// Create metadata message (use camelCase for protobufjs)
|
|
40
|
+
const metadataMsg = {
|
|
41
|
+
startTime: {
|
|
42
|
+
millisSinceEpoch: metadata.start_time.millis_since_epoch,
|
|
43
|
+
tzOffset: metadata.start_time.tz_offset
|
|
44
|
+
},
|
|
45
|
+
endTime: {
|
|
46
|
+
millisSinceEpoch: metadata.end_time.millis_since_epoch,
|
|
47
|
+
tzOffset: metadata.end_time.tz_offset
|
|
48
|
+
},
|
|
49
|
+
description: metadata.description,
|
|
50
|
+
hostname: metadata.hostname,
|
|
51
|
+
username: metadata.username,
|
|
52
|
+
isSnapshot: metadata.is_snapshot,
|
|
53
|
+
tags: metadata.tags
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
// Create operation message (use camelCase for protobufjs)
|
|
57
|
+
const message = Operation.create({
|
|
58
|
+
viewId: viewIdBuffer,
|
|
59
|
+
parents: parentBuffers,
|
|
60
|
+
metadata: metadataMsg,
|
|
61
|
+
commitPredecessors: [], // Empty for now
|
|
62
|
+
storesCommitPredecessors: false
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Verify the message
|
|
66
|
+
const errMsg = Operation.verify(message);
|
|
67
|
+
if (errMsg) {
|
|
68
|
+
throw new Error(`Operation message verification failed: ${errMsg}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Encode to binary
|
|
72
|
+
const buffer = Operation.encode(message).finish();
|
|
73
|
+
|
|
74
|
+
// Write to .jj/repo/op_store/operations/OPERATIONID
|
|
75
|
+
const opPath = `${this.dir}/.jj/repo/op_store/operations/${operationId}`;
|
|
76
|
+
await this.fs.promises.writeFile(opPath, buffer);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Read operation file
|
|
81
|
+
*
|
|
82
|
+
* @param {string} operationId - Operation ID as hex string (128 characters)
|
|
83
|
+
* @returns {Object} Decoded operation data
|
|
84
|
+
*/
|
|
85
|
+
async readOperation(operationId) {
|
|
86
|
+
// Read from .jj/repo/op_store/operations/OPERATIONID
|
|
87
|
+
const opPath = `${this.dir}/.jj/repo/op_store/operations/${operationId}`;
|
|
88
|
+
const buffer = await this.fs.promises.readFile(opPath);
|
|
89
|
+
|
|
90
|
+
// Load protobuf schema
|
|
91
|
+
const root = await protobuf.load(this.protoPath);
|
|
92
|
+
const Operation = root.lookupType('simple_op_store.Operation');
|
|
93
|
+
|
|
94
|
+
// Decode
|
|
95
|
+
const message = Operation.decode(buffer);
|
|
96
|
+
|
|
97
|
+
// Return the decoded message with proper field access
|
|
98
|
+
return {
|
|
99
|
+
view_id: message.viewId,
|
|
100
|
+
parents: message.parents,
|
|
101
|
+
metadata: {
|
|
102
|
+
start_time: {
|
|
103
|
+
millis_since_epoch: message.metadata.startTime.millisSinceEpoch,
|
|
104
|
+
tz_offset: message.metadata.startTime.tzOffset
|
|
105
|
+
},
|
|
106
|
+
end_time: {
|
|
107
|
+
millis_since_epoch: message.metadata.endTime.millisSinceEpoch,
|
|
108
|
+
tz_offset: message.metadata.endTime.tzOffset
|
|
109
|
+
},
|
|
110
|
+
description: message.metadata.description,
|
|
111
|
+
hostname: message.metadata.hostname,
|
|
112
|
+
username: message.metadata.username,
|
|
113
|
+
is_snapshot: message.metadata.isSnapshot,
|
|
114
|
+
tags: message.metadata.tags
|
|
115
|
+
},
|
|
116
|
+
commit_predecessors: message.commitPredecessors,
|
|
117
|
+
stores_commit_predecessors: message.storesCommitPredecessors
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|