isomorphic-jj 1.4.2 → 1.7.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/CHANGELOG.md +226 -0
- package/README.md +9 -11
- package/bin/isojj.js +270 -102
- package/dist/index.cjs +2949 -1676
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2949 -1676
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -4
- package/src/api/repository.js +1099 -739
- package/src/backend/isomorphic-git-backend.js +183 -136
- package/src/backend/lazy-git-backend.js +12 -19
- package/src/browser/helpers.js +6 -2
- package/src/core/background-ops.js +58 -41
- package/src/core/bookmark-store.js +12 -8
- package/src/core/change-graph.js +26 -29
- package/src/core/conflict-model.js +66 -26
- package/src/core/jj-checkout.js +8 -4
- package/src/core/jj-operation-store.js +16 -12
- package/src/core/jj-tree-state.js +13 -9
- package/src/core/jj-view-store.js +11 -5
- package/src/core/merge-driver-registry.js +92 -46
- package/src/core/operation-log.js +30 -33
- package/src/core/revset-engine.js +1069 -485
- package/src/core/storage-manager.js +25 -19
- package/src/core/tag-store.js +2 -2
- package/src/core/user-config.js +3 -1
- package/src/core/working-copy.js +125 -9
- package/src/core/workspace-manager.js +2 -2
- package/src/drivers/built-in-drivers.js +66 -33
- package/src/types.d.ts +67 -1
- package/src/utils/errors.js +3 -3
- package/src/utils/id-generation.js +4 -4
- package/src/utils/validation.js +6 -10
|
@@ -8,8 +8,8 @@ import { JJError } from '../utils/errors.js';
|
|
|
8
8
|
|
|
9
9
|
export class BackgroundOps {
|
|
10
10
|
/**
|
|
11
|
-
* @param {
|
|
12
|
-
* @param {
|
|
11
|
+
* @param {any} jj - JJ instance
|
|
12
|
+
* @param {any} fs - Filesystem implementation
|
|
13
13
|
* @param {string} dir - Repository directory
|
|
14
14
|
*/
|
|
15
15
|
constructor(jj, fs, dir) {
|
|
@@ -63,7 +63,7 @@ export class BackgroundOps {
|
|
|
63
63
|
*
|
|
64
64
|
* @param {string} path - Path to watch
|
|
65
65
|
* @param {Function} callback - Callback function(event, filename)
|
|
66
|
-
* @returns {string} Watcher ID
|
|
66
|
+
* @returns {Promise<string>} Watcher ID
|
|
67
67
|
*/
|
|
68
68
|
async watch(path, callback) {
|
|
69
69
|
if (!this.running) {
|
|
@@ -81,13 +81,15 @@ export class BackgroundOps {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
// Browser: use FileSystem Access API observeDirectory if available
|
|
84
|
-
if (typeof FileSystemObserver !== 'undefined') {
|
|
84
|
+
if (typeof (/** @type {any} */ (globalThis).FileSystemObserver) !== 'undefined') {
|
|
85
85
|
const watcherId = `watcher-${this.watchers.size}`;
|
|
86
|
-
const observer = new
|
|
87
|
-
|
|
88
|
-
|
|
86
|
+
const observer = new /** @type {any} */ (globalThis).FileSystemObserver(
|
|
87
|
+
async (/** @type {any} */ records) => {
|
|
88
|
+
for (const record of records) {
|
|
89
|
+
callback(record.type, record.relativePathComponents.join('/'));
|
|
90
|
+
}
|
|
89
91
|
}
|
|
90
|
-
|
|
92
|
+
);
|
|
91
93
|
|
|
92
94
|
// Note: This API is experimental and may not be available
|
|
93
95
|
this.watchers.set(watcherId, observer);
|
|
@@ -130,11 +132,12 @@ export class BackgroundOps {
|
|
|
130
132
|
* @param {Function} operation - Async operation to run
|
|
131
133
|
* @param {Object} opts - Options
|
|
132
134
|
* @param {string} [opts.description] - Operation description
|
|
133
|
-
* @returns {Promise<
|
|
135
|
+
* @returns {Promise<{id: string, promise: Promise<any>}>} Operation handle
|
|
134
136
|
*/
|
|
135
137
|
async queue(operation, opts = {}) {
|
|
136
138
|
const operationId = `op-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
137
139
|
|
|
140
|
+
/** @type {Record<string, any>} */
|
|
138
141
|
const op = {
|
|
139
142
|
id: operationId,
|
|
140
143
|
description: opts.description || 'background operation',
|
|
@@ -190,10 +193,10 @@ export class BackgroundOps {
|
|
|
190
193
|
let ops = Array.from(this.operations.values());
|
|
191
194
|
|
|
192
195
|
if (opts.status) {
|
|
193
|
-
ops = ops.filter(op => op.status === opts.status);
|
|
196
|
+
ops = ops.filter((op) => op.status === opts.status);
|
|
194
197
|
}
|
|
195
198
|
|
|
196
|
-
return ops.map(op => ({
|
|
199
|
+
return ops.map((op) => ({
|
|
197
200
|
id: op.id,
|
|
198
201
|
description: op.description,
|
|
199
202
|
status: op.status,
|
|
@@ -212,42 +215,56 @@ export class BackgroundOps {
|
|
|
212
215
|
async enableAutoSnapshot(opts = {}) {
|
|
213
216
|
const debounceMs = opts.debounceMs || 1000;
|
|
214
217
|
|
|
215
|
-
const watcherId = await this.watch(
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
218
|
+
const watcherId = await this.watch(
|
|
219
|
+
this.dir,
|
|
220
|
+
(/** @type {any} */ event, /** @type {any} */ filename) => {
|
|
221
|
+
if (filename && filename.startsWith('.jj/')) {
|
|
222
|
+
return; // Ignore .jj directory changes
|
|
223
|
+
}
|
|
219
224
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
225
|
+
if (filename && filename.startsWith('.git/')) {
|
|
226
|
+
return; // Ignore .git directory changes
|
|
227
|
+
}
|
|
223
228
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
+
// Debounce: wait for changes to settle
|
|
230
|
+
const existingTimer = this.timers.get(watcherId);
|
|
231
|
+
if (existingTimer) {
|
|
232
|
+
clearTimeout(existingTimer);
|
|
233
|
+
}
|
|
229
234
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
235
|
+
const timer = setTimeout(async () => {
|
|
236
|
+
try {
|
|
237
|
+
// Create automatic snapshot. queue() resolves as soon as the
|
|
238
|
+
// operation is *enqueued*, not once it finishes — it returns a
|
|
239
|
+
// `{ id, promise }` handle. Awaiting only queue() (and not the
|
|
240
|
+
// returned `promise`) would let a later describe() rejection
|
|
241
|
+
// become an unhandled promise rejection instead of landing in
|
|
242
|
+
// this catch block, so the handle's promise must be awaited too.
|
|
243
|
+
const { promise } = await this.queue(
|
|
244
|
+
async () => {
|
|
245
|
+
await this.jj.describe({
|
|
246
|
+
message: `Auto-snapshot: ${filename || 'multiple files'} changed`,
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
{ description: 'auto-snapshot' }
|
|
250
|
+
);
|
|
251
|
+
await promise;
|
|
252
|
+
} catch (error) {
|
|
253
|
+
console.error('Auto-snapshot failed:', error);
|
|
254
|
+
} finally {
|
|
255
|
+
// Clean up timer reference
|
|
256
|
+
this.timers.delete(watcherId);
|
|
257
|
+
}
|
|
258
|
+
}, debounceMs);
|
|
259
|
+
|
|
260
|
+
// Use unref() to allow process to exit if this is the only pending timer
|
|
261
|
+
if (timer.unref) {
|
|
262
|
+
timer.unref();
|
|
241
263
|
}
|
|
242
|
-
}, debounceMs);
|
|
243
264
|
|
|
244
|
-
|
|
245
|
-
if (timer.unref) {
|
|
246
|
-
timer.unref();
|
|
265
|
+
this.timers.set(watcherId, timer);
|
|
247
266
|
}
|
|
248
|
-
|
|
249
|
-
this.timers.set(watcherId, timer);
|
|
250
|
-
});
|
|
267
|
+
);
|
|
251
268
|
|
|
252
269
|
return watcherId;
|
|
253
270
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* BookmarkStore - Manages local and remote bookmarks
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Bookmarks are named pointers to changes, similar to Git branches.
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -15,6 +15,7 @@ export class BookmarkStore {
|
|
|
15
15
|
this.storage = storage;
|
|
16
16
|
this.local = new Map();
|
|
17
17
|
this.remote = new Map(); // remote → (bookmark → changeId)
|
|
18
|
+
/** @type {Record<string, any>} */
|
|
18
19
|
this.tracking = {}; // bookmark → { remote, ref } tracking info (v0.35.0)
|
|
19
20
|
}
|
|
20
21
|
|
|
@@ -79,11 +80,13 @@ export class BookmarkStore {
|
|
|
79
80
|
* Save bookmarks to storage
|
|
80
81
|
*/
|
|
81
82
|
async save() {
|
|
83
|
+
/** @type {Record<string, any>} */
|
|
82
84
|
const local = {};
|
|
83
85
|
for (const [name, target] of this.local.entries()) {
|
|
84
86
|
local[name] = target;
|
|
85
87
|
}
|
|
86
88
|
|
|
89
|
+
/** @type {Record<string, any>} */
|
|
87
90
|
const remote = {};
|
|
88
91
|
for (const [remoteName, bookmarks] of this.remote.entries()) {
|
|
89
92
|
remote[remoteName] = {};
|
|
@@ -102,7 +105,7 @@ export class BookmarkStore {
|
|
|
102
105
|
|
|
103
106
|
/**
|
|
104
107
|
* Set a local bookmark
|
|
105
|
-
*
|
|
108
|
+
*
|
|
106
109
|
* @param {string} name - Bookmark name
|
|
107
110
|
* @param {string} target - Change ID to point to
|
|
108
111
|
*/
|
|
@@ -123,7 +126,7 @@ export class BookmarkStore {
|
|
|
123
126
|
|
|
124
127
|
/**
|
|
125
128
|
* Move an existing local bookmark
|
|
126
|
-
*
|
|
129
|
+
*
|
|
127
130
|
* @param {string} name - Bookmark name
|
|
128
131
|
* @param {string} target - New change ID to point to
|
|
129
132
|
*/
|
|
@@ -144,7 +147,7 @@ export class BookmarkStore {
|
|
|
144
147
|
|
|
145
148
|
/**
|
|
146
149
|
* Delete a local bookmark
|
|
147
|
-
*
|
|
150
|
+
*
|
|
148
151
|
* @param {string} name - Bookmark name
|
|
149
152
|
*/
|
|
150
153
|
async delete(name) {
|
|
@@ -162,7 +165,7 @@ export class BookmarkStore {
|
|
|
162
165
|
|
|
163
166
|
/**
|
|
164
167
|
* Get bookmark target
|
|
165
|
-
*
|
|
168
|
+
*
|
|
166
169
|
* @param {string} name - Bookmark name
|
|
167
170
|
* @returns {Promise<string|null>} Change ID or null if not found
|
|
168
171
|
*/
|
|
@@ -172,7 +175,7 @@ export class BookmarkStore {
|
|
|
172
175
|
|
|
173
176
|
/**
|
|
174
177
|
* Set a remote bookmark
|
|
175
|
-
*
|
|
178
|
+
*
|
|
176
179
|
* @param {string} remoteName - Remote name
|
|
177
180
|
* @param {string} name - Bookmark name
|
|
178
181
|
* @param {string} target - Change ID to point to
|
|
@@ -191,7 +194,7 @@ export class BookmarkStore {
|
|
|
191
194
|
|
|
192
195
|
/**
|
|
193
196
|
* Get remote bookmark target
|
|
194
|
-
*
|
|
197
|
+
*
|
|
195
198
|
* @param {string} remoteName - Remote name
|
|
196
199
|
* @param {string} name - Bookmark name
|
|
197
200
|
* @returns {Promise<string|null>} Change ID or null if not found
|
|
@@ -204,13 +207,14 @@ export class BookmarkStore {
|
|
|
204
207
|
/**
|
|
205
208
|
* List all bookmarks (local and remote)
|
|
206
209
|
*
|
|
207
|
-
* @returns {Promise<Array
|
|
210
|
+
* @returns {Promise<Array<any>>} Array of bookmark objects
|
|
208
211
|
*/
|
|
209
212
|
async list() {
|
|
210
213
|
const result = [];
|
|
211
214
|
|
|
212
215
|
// Add local bookmarks with tracking info (v0.35.0)
|
|
213
216
|
for (const [name, target] of this.local.entries()) {
|
|
217
|
+
/** @type {Record<string, any>} */
|
|
214
218
|
const bookmark = {
|
|
215
219
|
name,
|
|
216
220
|
changeId: target,
|
package/src/core/change-graph.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* ChangeGraph - Manages the change graph with stable change IDs
|
|
3
|
-
*
|
|
3
|
+
*
|
|
4
4
|
* Tracks changes, their relationships (parents/children), and evolution (amends/rewrites).
|
|
5
5
|
*/
|
|
6
6
|
|
|
@@ -62,6 +62,7 @@ export class ChangeGraph {
|
|
|
62
62
|
* Save graph to storage
|
|
63
63
|
*/
|
|
64
64
|
async save() {
|
|
65
|
+
/** @type {Record<string, any>} */
|
|
65
66
|
const changes = {};
|
|
66
67
|
for (const [changeId, change] of this.nodes.entries()) {
|
|
67
68
|
changes[changeId] = change;
|
|
@@ -75,8 +76,8 @@ export class ChangeGraph {
|
|
|
75
76
|
|
|
76
77
|
/**
|
|
77
78
|
* Add a change to the graph
|
|
78
|
-
*
|
|
79
|
-
* @param {
|
|
79
|
+
*
|
|
80
|
+
* @param {Record<string, any>} change - Change object
|
|
80
81
|
*/
|
|
81
82
|
async addChange(change) {
|
|
82
83
|
validateChangeId(change.changeId);
|
|
@@ -96,9 +97,9 @@ export class ChangeGraph {
|
|
|
96
97
|
|
|
97
98
|
/**
|
|
98
99
|
* Get a change by ID
|
|
99
|
-
*
|
|
100
|
+
*
|
|
100
101
|
* @param {string} changeId - Change ID
|
|
101
|
-
* @returns {
|
|
102
|
+
* @returns {Promise<Record<string, any>|null>} Change object or null if not found
|
|
102
103
|
*/
|
|
103
104
|
async getChange(changeId) {
|
|
104
105
|
validateChangeId(changeId);
|
|
@@ -107,8 +108,8 @@ export class ChangeGraph {
|
|
|
107
108
|
|
|
108
109
|
/**
|
|
109
110
|
* Get all changes
|
|
110
|
-
*
|
|
111
|
-
* @returns {Array} Array of all changes
|
|
111
|
+
*
|
|
112
|
+
* @returns {Array<any>} Array of all changes
|
|
112
113
|
*/
|
|
113
114
|
getAll() {
|
|
114
115
|
return Array.from(this.nodes.values());
|
|
@@ -116,7 +117,7 @@ export class ChangeGraph {
|
|
|
116
117
|
|
|
117
118
|
/**
|
|
118
119
|
* Find change by commit ID
|
|
119
|
-
*
|
|
120
|
+
*
|
|
120
121
|
* @param {string} commitId - Git commit SHA-1
|
|
121
122
|
* @returns {string|null} Change ID or null if not found
|
|
122
123
|
*/
|
|
@@ -126,8 +127,8 @@ export class ChangeGraph {
|
|
|
126
127
|
|
|
127
128
|
/**
|
|
128
129
|
* Get parent change IDs
|
|
129
|
-
*
|
|
130
|
-
* @param {
|
|
130
|
+
*
|
|
131
|
+
* @param {any} changeId - Change ID
|
|
131
132
|
* @returns {Array<string>} Array of parent change IDs
|
|
132
133
|
*/
|
|
133
134
|
getParents(changeId) {
|
|
@@ -137,27 +138,27 @@ export class ChangeGraph {
|
|
|
137
138
|
|
|
138
139
|
/**
|
|
139
140
|
* Get child change IDs
|
|
140
|
-
*
|
|
141
|
+
*
|
|
141
142
|
* @param {string} changeId - Change ID
|
|
142
143
|
* @returns {Array<string>} Array of child change IDs
|
|
143
144
|
*/
|
|
144
145
|
getChildren(changeId) {
|
|
145
146
|
const children = [];
|
|
146
|
-
|
|
147
|
+
|
|
147
148
|
for (const change of this.nodes.values()) {
|
|
148
149
|
if (change.parents.includes(changeId)) {
|
|
149
150
|
children.push(change.changeId);
|
|
150
151
|
}
|
|
151
152
|
}
|
|
152
|
-
|
|
153
|
+
|
|
153
154
|
return children;
|
|
154
155
|
}
|
|
155
156
|
|
|
156
157
|
/**
|
|
157
158
|
* Evolve a change (update commitId, track predecessor)
|
|
158
|
-
*
|
|
159
|
+
*
|
|
159
160
|
* Used when amending or rewriting a change.
|
|
160
|
-
*
|
|
161
|
+
*
|
|
161
162
|
* @param {string} changeId - Change ID
|
|
162
163
|
* @param {string} newCommitId - New commit SHA-1
|
|
163
164
|
*/
|
|
@@ -188,8 +189,8 @@ export class ChangeGraph {
|
|
|
188
189
|
|
|
189
190
|
/**
|
|
190
191
|
* Update a change (modify in place)
|
|
191
|
-
*
|
|
192
|
-
* @param {
|
|
192
|
+
*
|
|
193
|
+
* @param {Record<string, any>} change - Updated change object
|
|
193
194
|
*/
|
|
194
195
|
async updateChange(change) {
|
|
195
196
|
validateChangeId(change.changeId);
|
|
@@ -213,20 +214,19 @@ export class ChangeGraph {
|
|
|
213
214
|
*/
|
|
214
215
|
getAncestors(changeId) {
|
|
215
216
|
const ancestors = [];
|
|
216
|
-
|
|
217
|
+
// `visited` also doubles as "already enqueued" — mark a node the moment
|
|
218
|
+
// it's scheduled (not when dequeued), so a change reachable via two paths
|
|
219
|
+
// in a diamond-shaped history (a<-b, a<-c, b&c<-d) is only added once.
|
|
220
|
+
const visited = new Set([changeId]);
|
|
217
221
|
const queue = [changeId];
|
|
218
222
|
|
|
219
223
|
while (queue.length > 0) {
|
|
220
224
|
const current = queue.shift();
|
|
221
225
|
|
|
222
|
-
if (visited.has(current)) {
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
visited.add(current);
|
|
226
|
-
|
|
227
226
|
const parents = this.getParents(current);
|
|
228
227
|
for (const parent of parents) {
|
|
229
228
|
if (!visited.has(parent)) {
|
|
229
|
+
visited.add(parent);
|
|
230
230
|
ancestors.push(parent);
|
|
231
231
|
queue.push(parent);
|
|
232
232
|
}
|
|
@@ -239,12 +239,8 @@ export class ChangeGraph {
|
|
|
239
239
|
/**
|
|
240
240
|
* Create a change with default values (helper for tests)
|
|
241
241
|
*
|
|
242
|
-
* @param {
|
|
243
|
-
* @
|
|
244
|
-
* @param {string} [params.description] - Change description
|
|
245
|
-
* @param {Object} [params.fileSnapshot] - File snapshot
|
|
246
|
-
* @param {Object} [params.conflicts] - Conflict markers
|
|
247
|
-
* @returns {Promise<Object>} Created change object
|
|
242
|
+
* @param {Record<string, any>} [params] - Change parameters
|
|
243
|
+
* @returns {Promise<Record<string, any>>} Created change object
|
|
248
244
|
*/
|
|
249
245
|
async createChange(params = {}) {
|
|
250
246
|
const crypto = await import('crypto');
|
|
@@ -253,6 +249,7 @@ export class ChangeGraph {
|
|
|
253
249
|
const tree = params.tree || crypto.randomBytes(20).toString('hex');
|
|
254
250
|
const timestamp = params.timestamp || new Date().toISOString();
|
|
255
251
|
|
|
252
|
+
/** @type {Record<string, any>} */
|
|
256
253
|
const change = {
|
|
257
254
|
changeId,
|
|
258
255
|
parents: params.parents || [],
|
|
@@ -18,20 +18,26 @@ import { generateId } from '../utils/id-generation.js';
|
|
|
18
18
|
* Conflict types supported by JJ
|
|
19
19
|
*/
|
|
20
20
|
export const ConflictType = {
|
|
21
|
-
CONTENT: 'content',
|
|
22
|
-
PATH: 'path',
|
|
23
|
-
DELETE_MODIFY: 'delete-modify',
|
|
24
|
-
MODIFY_DELETE: 'modify-delete',
|
|
25
|
-
ADD_ADD: 'add-add',
|
|
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
26
|
};
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
29
|
* ConflictModel manages conflict detection, storage, and resolution
|
|
30
30
|
*/
|
|
31
31
|
export class ConflictModel {
|
|
32
|
+
/**
|
|
33
|
+
* @param {any} storage
|
|
34
|
+
* @param {any} fs
|
|
35
|
+
* @param {any} [mergeDriverRegistry]
|
|
36
|
+
*/
|
|
32
37
|
constructor(storage, fs, mergeDriverRegistry = null) {
|
|
33
38
|
this.storage = storage;
|
|
34
39
|
this.fs = fs;
|
|
40
|
+
/** @type {any} */
|
|
35
41
|
this.mergeDriverRegistry = mergeDriverRegistry; // v0.5: merge drivers
|
|
36
42
|
this.conflicts = new Map(); // conflictId -> Conflict
|
|
37
43
|
this.fileConflicts = new Map(); // path -> conflictId
|
|
@@ -81,23 +87,29 @@ export class ConflictModel {
|
|
|
81
87
|
* @param {string} opts.baseChange - Common ancestor change
|
|
82
88
|
* @param {string} opts.leftChange - Left side change
|
|
83
89
|
* @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
|
|
90
|
+
* @param {Map<any, any>} opts.baseFiles - Files in base
|
|
91
|
+
* @param {Map<any, any>} opts.leftFiles - Files in left
|
|
92
|
+
* @param {Map<any, any>} opts.rightFiles - Files in right
|
|
87
93
|
* @param {Object} opts.drivers - Custom merge drivers (v0.5)
|
|
88
94
|
* @param {string} opts.workingCopyDir - Working copy directory for file writes (v0.5)
|
|
89
|
-
* @returns {
|
|
95
|
+
* @returns {Promise<any[]>} Detected conflicts
|
|
90
96
|
*/
|
|
91
97
|
async detectConflicts(opts) {
|
|
92
|
-
const {
|
|
98
|
+
const {
|
|
99
|
+
baseFiles,
|
|
100
|
+
leftFiles,
|
|
101
|
+
rightFiles,
|
|
102
|
+
drivers = {},
|
|
103
|
+
workingCopyDir,
|
|
104
|
+
baseChange,
|
|
105
|
+
leftChange,
|
|
106
|
+
rightChange,
|
|
107
|
+
} = opts;
|
|
108
|
+
/** @type {any[]} */
|
|
93
109
|
const conflicts = [];
|
|
94
110
|
|
|
95
111
|
// Get all unique paths across all versions
|
|
96
|
-
const allPaths = new Set([
|
|
97
|
-
...baseFiles.keys(),
|
|
98
|
-
...leftFiles.keys(),
|
|
99
|
-
...rightFiles.keys(),
|
|
100
|
-
]);
|
|
112
|
+
const allPaths = new Set([...baseFiles.keys(), ...leftFiles.keys(), ...rightFiles.keys()]);
|
|
101
113
|
|
|
102
114
|
for (const path of allPaths) {
|
|
103
115
|
const baseContent = baseFiles.get(path);
|
|
@@ -108,7 +120,7 @@ export class ConflictModel {
|
|
|
108
120
|
if (this.mergeDriverRegistry) {
|
|
109
121
|
const mergeResult = await this._tryMergeDriver(
|
|
110
122
|
path,
|
|
111
|
-
{base: baseContent, ours: leftContent, theirs: rightContent},
|
|
123
|
+
{ base: baseContent, ours: leftContent, theirs: rightContent },
|
|
112
124
|
drivers,
|
|
113
125
|
{ baseChange, leftChange, rightChange }
|
|
114
126
|
);
|
|
@@ -125,7 +137,10 @@ export class ConflictModel {
|
|
|
125
137
|
type: driverConflict.type || 'driver-conflict',
|
|
126
138
|
path,
|
|
127
139
|
sides: { base: baseContent, left: leftContent, right: rightContent },
|
|
128
|
-
message:
|
|
140
|
+
message:
|
|
141
|
+
driverConflict.message ||
|
|
142
|
+
mergeResult.message ||
|
|
143
|
+
`Merge driver detected conflicts`,
|
|
129
144
|
driverResult: mergeResult,
|
|
130
145
|
driverFailed: mergeResult.driverFailed,
|
|
131
146
|
driverError: mergeResult.driverError,
|
|
@@ -167,6 +182,11 @@ export class ConflictModel {
|
|
|
167
182
|
|
|
168
183
|
/**
|
|
169
184
|
* Detect conflict for a single path
|
|
185
|
+
*
|
|
186
|
+
* @param {any} path
|
|
187
|
+
* @param {any} base
|
|
188
|
+
* @param {any} left
|
|
189
|
+
* @param {any} right
|
|
170
190
|
*/
|
|
171
191
|
_detectPathConflict(path, base, left, right) {
|
|
172
192
|
const baseExists = base !== undefined;
|
|
@@ -243,9 +263,12 @@ export class ConflictModel {
|
|
|
243
263
|
|
|
244
264
|
/**
|
|
245
265
|
* Create a conflict object
|
|
266
|
+
*
|
|
267
|
+
* @param {{ type: any, path: any, sides: any, message: any, driverFailed?: any, driverError?: any, driverResult?: any }} args
|
|
246
268
|
*/
|
|
247
269
|
_createConflict({ type, path, sides, message, driverFailed, driverError }) {
|
|
248
270
|
const conflictId = generateId('conflict');
|
|
271
|
+
/** @type {Record<string, any>} */
|
|
249
272
|
const conflict = {
|
|
250
273
|
conflictId,
|
|
251
274
|
type,
|
|
@@ -269,6 +292,8 @@ export class ConflictModel {
|
|
|
269
292
|
|
|
270
293
|
/**
|
|
271
294
|
* Store a conflict
|
|
295
|
+
*
|
|
296
|
+
* @param {any} conflict
|
|
272
297
|
*/
|
|
273
298
|
async addConflict(conflict) {
|
|
274
299
|
this.conflicts.set(conflict.conflictId, conflict);
|
|
@@ -279,6 +304,8 @@ export class ConflictModel {
|
|
|
279
304
|
|
|
280
305
|
/**
|
|
281
306
|
* Get conflict by ID
|
|
307
|
+
*
|
|
308
|
+
* @param {any} conflictId
|
|
282
309
|
*/
|
|
283
310
|
getConflict(conflictId) {
|
|
284
311
|
return this.conflicts.get(conflictId);
|
|
@@ -286,6 +313,8 @@ export class ConflictModel {
|
|
|
286
313
|
|
|
287
314
|
/**
|
|
288
315
|
* Get conflict for a file path
|
|
316
|
+
*
|
|
317
|
+
* @param {any} path
|
|
289
318
|
*/
|
|
290
319
|
getConflictForPath(path) {
|
|
291
320
|
const conflictId = this.fileConflicts.get(path);
|
|
@@ -295,16 +324,18 @@ export class ConflictModel {
|
|
|
295
324
|
|
|
296
325
|
/**
|
|
297
326
|
* List all conflicts
|
|
327
|
+
*
|
|
328
|
+
* @param {Record<string, any>} [opts]
|
|
298
329
|
*/
|
|
299
330
|
listConflicts(opts = {}) {
|
|
300
331
|
let conflicts = Array.from(this.conflicts.values());
|
|
301
332
|
|
|
302
333
|
if (opts.resolved !== undefined) {
|
|
303
|
-
conflicts = conflicts.filter(c => c.resolved === opts.resolved);
|
|
334
|
+
conflicts = conflicts.filter((c) => c.resolved === opts.resolved);
|
|
304
335
|
}
|
|
305
336
|
|
|
306
337
|
if (opts.type) {
|
|
307
|
-
conflicts = conflicts.filter(c => c.type === opts.type);
|
|
338
|
+
conflicts = conflicts.filter((c) => c.type === opts.type);
|
|
308
339
|
}
|
|
309
340
|
|
|
310
341
|
return conflicts;
|
|
@@ -312,6 +343,9 @@ export class ConflictModel {
|
|
|
312
343
|
|
|
313
344
|
/**
|
|
314
345
|
* Mark a conflict as resolved
|
|
346
|
+
*
|
|
347
|
+
* @param {any} conflictId
|
|
348
|
+
* @param {any} resolution
|
|
315
349
|
*/
|
|
316
350
|
async resolveConflict(conflictId, resolution) {
|
|
317
351
|
const conflict = this.conflicts.get(conflictId);
|
|
@@ -331,7 +365,7 @@ export class ConflictModel {
|
|
|
331
365
|
// { side: 'ours' | 'theirs' | 'base' }
|
|
332
366
|
if (!['ours', 'theirs', 'base'].includes(resolution.side)) {
|
|
333
367
|
throw new JJError('INVALID_RESOLUTION', `Invalid side: ${resolution.side}`, {
|
|
334
|
-
suggestion: 'Use "ours", "theirs", or "base"'
|
|
368
|
+
suggestion: 'Use "ours", "theirs", or "base"',
|
|
335
369
|
});
|
|
336
370
|
}
|
|
337
371
|
resolvedContent = { type: 'side', side: resolution.side };
|
|
@@ -359,6 +393,8 @@ export class ConflictModel {
|
|
|
359
393
|
|
|
360
394
|
/**
|
|
361
395
|
* Remove a conflict (after resolution is committed)
|
|
396
|
+
*
|
|
397
|
+
* @param {any} conflictId
|
|
362
398
|
*/
|
|
363
399
|
async removeConflict(conflictId) {
|
|
364
400
|
const conflict = this.conflicts.get(conflictId);
|
|
@@ -378,11 +414,12 @@ export class ConflictModel {
|
|
|
378
414
|
|
|
379
415
|
/**
|
|
380
416
|
* Create conflict markers for a content conflict (like Git's <<<<<<<, =======, >>>>>>>)
|
|
417
|
+
*
|
|
418
|
+
* @param {any} conflict
|
|
381
419
|
*/
|
|
382
420
|
generateConflictMarkers(conflict) {
|
|
383
421
|
if (conflict.type !== ConflictType.CONTENT) {
|
|
384
|
-
throw new JJError('INVALID_CONFLICT_TYPE',
|
|
385
|
-
'Can only generate markers for content conflicts');
|
|
422
|
+
throw new JJError('INVALID_CONFLICT_TYPE', 'Can only generate markers for content conflicts');
|
|
386
423
|
}
|
|
387
424
|
|
|
388
425
|
const { base, left, right } = conflict.sides;
|
|
@@ -400,9 +437,12 @@ export class ConflictModel {
|
|
|
400
437
|
|
|
401
438
|
/**
|
|
402
439
|
* Parse conflict markers from file content
|
|
440
|
+
*
|
|
441
|
+
* @param {any} content
|
|
403
442
|
*/
|
|
404
443
|
parseConflictMarkers(content) {
|
|
405
|
-
const markerRegex =
|
|
444
|
+
const markerRegex =
|
|
445
|
+
/^<{7} Left\n([\s\S]*?)\n\|{7} Base\n([\s\S]*?)\n={7}\n([\s\S]*?)\n>{7} Right$/gm;
|
|
406
446
|
const matches = markerRegex.exec(content);
|
|
407
447
|
|
|
408
448
|
if (!matches) {
|
|
@@ -429,10 +469,10 @@ export class ConflictModel {
|
|
|
429
469
|
* Try to use a merge driver for a file (v0.5)
|
|
430
470
|
*
|
|
431
471
|
* @param {string} path - File path
|
|
432
|
-
* @param {
|
|
472
|
+
* @param {{ base?: string, ours?: string, theirs?: string }} content - Content versions { base, ours, theirs }
|
|
433
473
|
* @param {Object} customDrivers - Per-merge custom drivers
|
|
434
474
|
* @param {Object} metadata - Merge metadata
|
|
435
|
-
* @returns {Promise<
|
|
475
|
+
* @returns {Promise<Record<string, any>|null>} Merge result or null if no driver
|
|
436
476
|
*/
|
|
437
477
|
async _tryMergeDriver(path, content, customDrivers, metadata) {
|
|
438
478
|
if (!this.mergeDriverRegistry) {
|
|
@@ -469,7 +509,7 @@ export class ConflictModel {
|
|
|
469
509
|
*
|
|
470
510
|
* @param {string} workingCopyDir - Working copy directory
|
|
471
511
|
* @param {string} filePath - File path
|
|
472
|
-
* @param {
|
|
512
|
+
* @param {Record<string, any>} result - Merge result
|
|
473
513
|
*/
|
|
474
514
|
async _writeDriverResult(workingCopyDir, filePath, result) {
|
|
475
515
|
const fullPath = path.join(workingCopyDir, filePath);
|