isomorphic-jj 1.4.3 → 1.8.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 +428 -0
- package/README.md +138 -29
- package/bin/isojj.js +273 -102
- package/dist/index.cjs +3401 -1683
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +3401 -1683
- package/dist/index.mjs.map +1 -1
- package/package.json +21 -7
- package/src/api/repository.js +1301 -745
- package/src/backend/isomorphic-git-backend.js +241 -138
- package/src/backend/lazy-git-backend.js +12 -19
- package/src/browser/helpers.js +8 -4
- 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 +21 -14
- package/src/core/jj-tree-state.js +16 -11
- package/src/core/jj-view-store.js +14 -7
- package/src/core/merge-driver-registry.js +92 -46
- package/src/core/operation-log.js +30 -33
- package/src/core/revset-engine.js +1114 -485
- package/src/core/storage-manager.js +73 -30
- package/src/core/tag-store.js +42 -12
- 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/atomic-write.js +44 -0
- package/src/utils/errors.js +3 -3
- package/src/utils/id-generation.js +4 -4
- package/src/utils/mutex.js +63 -0
- package/src/utils/validation.js +6 -10
package/src/api/repository.js
CHANGED
|
@@ -23,13 +23,14 @@ import path from 'path';
|
|
|
23
23
|
* Create and initialize a JJ repository instance
|
|
24
24
|
*
|
|
25
25
|
* @param {Object} options - Configuration options
|
|
26
|
-
* @param {
|
|
26
|
+
* @param {any} options.fs - Filesystem implementation (Node fs or LightningFS)
|
|
27
27
|
* @param {string} options.dir - Repository directory path
|
|
28
|
-
* @param {
|
|
29
|
-
* @param {
|
|
30
|
-
* @param {string|
|
|
28
|
+
* @param {any} [options.git] - isomorphic-git instance (enables Git backend)
|
|
29
|
+
* @param {any} [options.http] - HTTP client for network operations
|
|
30
|
+
* @param {string|any} [options.backend] - Backend name ('isomorphic-git', 'memory') or backend instance
|
|
31
|
+
* @param {boolean} [options.autoSnapshot] - Auto-snapshot the working dir before read/commit ops (default true)
|
|
31
32
|
*
|
|
32
|
-
* @returns {Promise<
|
|
33
|
+
* @returns {Promise<Record<string, any>>} Initialized JJ instance
|
|
33
34
|
*/
|
|
34
35
|
export async function createJJ(options) {
|
|
35
36
|
if (!options) {
|
|
@@ -43,6 +44,11 @@ export async function createJJ(options) {
|
|
|
43
44
|
const git = options.git;
|
|
44
45
|
const http = options.http;
|
|
45
46
|
const backend = options.backend;
|
|
47
|
+
// v1.6: automatically snapshot the working directory (disk-walk) before
|
|
48
|
+
// read/commit operations, so files created out-of-band (editor, shell, git
|
|
49
|
+
// checkout) are picked up — matching jj's "snapshot before every command".
|
|
50
|
+
// Enabled by default; pass { autoSnapshot: false } to opt out.
|
|
51
|
+
const autoSnapshot = options.autoSnapshot !== false;
|
|
46
52
|
|
|
47
53
|
if (!fs) {
|
|
48
54
|
throw new JJError('INVALID_CONFIG', 'Missing fs', {
|
|
@@ -57,7 +63,7 @@ export async function createJJ(options) {
|
|
|
57
63
|
}
|
|
58
64
|
|
|
59
65
|
// Create core components
|
|
60
|
-
const storage = new Storage(fs, dir);
|
|
66
|
+
const storage = /** @type {any} */ (new Storage(fs, dir));
|
|
61
67
|
const baseGraph = new ChangeGraph(storage);
|
|
62
68
|
const workingCopy = new WorkingCopy(storage, fs, dir);
|
|
63
69
|
const oplog = new OperationLog(storage);
|
|
@@ -74,7 +80,7 @@ export async function createJJ(options) {
|
|
|
74
80
|
*/
|
|
75
81
|
const getUserOplogInfo = async () => {
|
|
76
82
|
await userConfig.load();
|
|
77
|
-
const user = userConfig.getUser();
|
|
83
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
78
84
|
return { name: user.name, email: user.email, hostname: 'localhost' };
|
|
79
85
|
};
|
|
80
86
|
|
|
@@ -124,14 +130,58 @@ export async function createJJ(options) {
|
|
|
124
130
|
return fileSnapshot;
|
|
125
131
|
};
|
|
126
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Automatic working-copy snapshot (v1.6).
|
|
135
|
+
*
|
|
136
|
+
* Walks the working directory on disk and reconciles tracked file state so
|
|
137
|
+
* that files created/modified/deleted outside of jj.write() are reflected in
|
|
138
|
+
* status/describe/diff/file.* . No-op when autoSnapshot is disabled or the
|
|
139
|
+
* working copy isn't initialized yet. Best-effort: never throws.
|
|
140
|
+
*
|
|
141
|
+
* @returns {Promise<{added: string[], modified: string[], deleted: string[]} | null>}
|
|
142
|
+
*/
|
|
143
|
+
const autoSnapshotWorkingCopy = async () => {
|
|
144
|
+
if (!autoSnapshot) return null;
|
|
145
|
+
try {
|
|
146
|
+
await workingCopy.load();
|
|
147
|
+
} catch {
|
|
148
|
+
return null; // repo not initialized yet
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let result;
|
|
152
|
+
try {
|
|
153
|
+
result = await workingCopy.snapshot();
|
|
154
|
+
} catch {
|
|
155
|
+
return null; // best-effort; don't let snapshotting break the operation
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// If the disk changed, refresh the working-copy change's content snapshot so
|
|
159
|
+
// read()/file.list()/file.search()/diff() reflect out-of-band edits.
|
|
160
|
+
if (result && (result.added.length || result.modified.length || result.deleted.length)) {
|
|
161
|
+
try {
|
|
162
|
+
await graph.load();
|
|
163
|
+
const wcId = workingCopy.getCurrentChangeId();
|
|
164
|
+
const change = await graph.getChange(wcId);
|
|
165
|
+
if (change) {
|
|
166
|
+
change.fileSnapshot = await snapshotFilesystem();
|
|
167
|
+
await graph.updateChange(change);
|
|
168
|
+
}
|
|
169
|
+
} catch {
|
|
170
|
+
// Best-effort content refresh; tracking state is already updated.
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return result;
|
|
175
|
+
};
|
|
176
|
+
|
|
127
177
|
/**
|
|
128
178
|
* Helper to dispatch events with async listener support and error handling
|
|
129
179
|
* Store listener errors in detail so we can check them after dispatch
|
|
130
|
-
* @param {
|
|
180
|
+
* @param {any} eventTarget
|
|
131
181
|
* @param {string} eventName
|
|
132
182
|
* @param {any} detail
|
|
133
183
|
* @param {{ cancelable?: boolean }} [options]
|
|
134
|
-
* @returns {Promise<
|
|
184
|
+
* @returns {Promise<any>}
|
|
135
185
|
*/
|
|
136
186
|
const dispatchEventAsync = async (eventTarget, eventName, detail, options = {}) => {
|
|
137
187
|
// Extend detail to track errors from listeners
|
|
@@ -159,11 +209,10 @@ export async function createJJ(options) {
|
|
|
159
209
|
|
|
160
210
|
// Check if event was prevented (only relevant for cancelable events)
|
|
161
211
|
if (!notCancelled && options.cancelable !== false) {
|
|
162
|
-
throw new JJError(
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
);
|
|
212
|
+
throw new JJError('EVENT_CANCELLED', `Operation cancelled by ${eventName} event listener`, {
|
|
213
|
+
eventName,
|
|
214
|
+
detail,
|
|
215
|
+
});
|
|
167
216
|
}
|
|
168
217
|
|
|
169
218
|
return event;
|
|
@@ -171,6 +220,7 @@ export async function createJJ(options) {
|
|
|
171
220
|
|
|
172
221
|
// Helper to sync a JJ change to a Git commit
|
|
173
222
|
// In JJ, every change has a corresponding Git commit
|
|
223
|
+
/** @param {any} change */
|
|
174
224
|
const syncChangeToGit = async (change) => {
|
|
175
225
|
if (!gitBackend || !gitBackend.createCommit) {
|
|
176
226
|
return; // No Git backend, skip
|
|
@@ -186,8 +236,12 @@ export async function createJJ(options) {
|
|
|
186
236
|
const parentCommitIds = [];
|
|
187
237
|
await baseGraph.load();
|
|
188
238
|
for (const parentChangeId of change.parents) {
|
|
189
|
-
const parentChange = await baseGraph.getChange(parentChangeId);
|
|
190
|
-
if (
|
|
239
|
+
const parentChange = /** @type {any} */ (await baseGraph.getChange(parentChangeId));
|
|
240
|
+
if (
|
|
241
|
+
parentChange &&
|
|
242
|
+
parentChange.commitId &&
|
|
243
|
+
parentChange.commitId !== '0000000000000000000000000000000000000000'
|
|
244
|
+
) {
|
|
191
245
|
parentCommitIds.push(parentChange.commitId);
|
|
192
246
|
}
|
|
193
247
|
}
|
|
@@ -229,31 +283,31 @@ export async function createJJ(options) {
|
|
|
229
283
|
* Create a middleware-wrapped graph that intercepts add/update operations
|
|
230
284
|
* This allows pluggable backends without modifying core ChangeGraph logic
|
|
231
285
|
*/
|
|
232
|
-
const createGraphWithMiddleware = (baseGraph, hooks) => {
|
|
286
|
+
const createGraphWithMiddleware = (/** @type {any} */ baseGraph, /** @type {any} */ hooks) => {
|
|
233
287
|
return {
|
|
234
288
|
// Delegate all read operations directly to base graph
|
|
235
289
|
load: () => baseGraph.load(),
|
|
236
|
-
getChange: (changeId) => baseGraph.getChange(changeId),
|
|
290
|
+
getChange: (/** @type {any} */ changeId) => baseGraph.getChange(changeId),
|
|
237
291
|
getAllChanges: () => baseGraph.getAll(), // Alias for compatibility
|
|
238
|
-
getAll: () => baseGraph.getAll(),
|
|
239
|
-
getAncestors: (changeId) => baseGraph.getAncestors(changeId),
|
|
240
|
-
getDescendants: (changeId) => baseGraph.getDescendants(changeId),
|
|
241
|
-
getParents: (changeId) => baseGraph.getParents(changeId),
|
|
242
|
-
getChildren: (changeId) => baseGraph.getChildren(changeId),
|
|
243
|
-
findChangeByCommitId: async (commitId) => {
|
|
292
|
+
getAll: () => baseGraph.getAll(), // Used by revset engine
|
|
293
|
+
getAncestors: (/** @type {any} */ changeId) => baseGraph.getAncestors(changeId),
|
|
294
|
+
getDescendants: (/** @type {any} */ changeId) => baseGraph.getDescendants(changeId),
|
|
295
|
+
getParents: (/** @type {any} */ changeId) => baseGraph.getParents(changeId),
|
|
296
|
+
getChildren: (/** @type {any} */ changeId) => baseGraph.getChildren(changeId),
|
|
297
|
+
findChangeByCommitId: async (/** @type {any} */ commitId) => {
|
|
244
298
|
const changeId = baseGraph.findByCommitId(commitId);
|
|
245
299
|
return changeId ? await baseGraph.getChange(changeId) : null;
|
|
246
300
|
},
|
|
247
301
|
|
|
248
302
|
// Wrap write operations with middleware hooks
|
|
249
|
-
async addChange(change) {
|
|
303
|
+
async addChange(/** @type {any} */ change) {
|
|
250
304
|
await baseGraph.addChange(change);
|
|
251
305
|
if (hooks.onAddChange) {
|
|
252
306
|
await hooks.onAddChange(change);
|
|
253
307
|
}
|
|
254
308
|
},
|
|
255
309
|
|
|
256
|
-
async updateChange(change) {
|
|
310
|
+
async updateChange(/** @type {any} */ change) {
|
|
257
311
|
await baseGraph.updateChange(change);
|
|
258
312
|
if (hooks.onUpdateChange) {
|
|
259
313
|
await hooks.onUpdateChange(change);
|
|
@@ -267,6 +321,7 @@ export async function createJJ(options) {
|
|
|
267
321
|
|
|
268
322
|
// Create Git backend if specified
|
|
269
323
|
// Auto-detect: if git instance provided, use isomorphic-git backend
|
|
324
|
+
/** @type {any} */
|
|
270
325
|
let gitBackend = null;
|
|
271
326
|
if (git || backend === 'isomorphic-git' || backend === 'git') {
|
|
272
327
|
gitBackend = new IsomorphicGitBackend({
|
|
@@ -282,23 +337,23 @@ export async function createJJ(options) {
|
|
|
282
337
|
// Wrap the base graph with middleware that syncs to Git
|
|
283
338
|
// This allows us to support different backends in the future
|
|
284
339
|
const graph = createGraphWithMiddleware(baseGraph, {
|
|
285
|
-
onAddChange: async (change) => {
|
|
340
|
+
onAddChange: async (/** @type {any} */ change) => {
|
|
286
341
|
// Sync new change to Git backend
|
|
287
342
|
await syncChangeToGit(change);
|
|
288
343
|
},
|
|
289
|
-
onUpdateChange: async (change) => {
|
|
344
|
+
onUpdateChange: async (/** @type {any} */ change) => {
|
|
290
345
|
// Sync updated change to Git backend
|
|
291
346
|
await syncChangeToGit(change);
|
|
292
347
|
},
|
|
293
348
|
});
|
|
294
349
|
|
|
295
350
|
// Create revset engine with the middleware-wrapped graph (v0.4: added bookmarkStore)
|
|
296
|
-
const revset = new RevsetEngine(graph, workingCopy, userConfig, bookmarks);
|
|
351
|
+
const revset = new RevsetEngine(graph, workingCopy, userConfig, bookmarks, tags);
|
|
297
352
|
|
|
298
353
|
/**
|
|
299
354
|
* Helper to resolve conflicts with different strategies (v0.5)
|
|
300
355
|
*
|
|
301
|
-
* @param {
|
|
356
|
+
* @param {any} conflict - Conflict object
|
|
302
357
|
* @param {string} strategy - Resolution strategy ('ours', 'theirs', 'union')
|
|
303
358
|
* @returns {string} Resolved content
|
|
304
359
|
*/
|
|
@@ -323,27 +378,31 @@ export async function createJJ(options) {
|
|
|
323
378
|
}
|
|
324
379
|
|
|
325
380
|
// Create JJ instance (backgroundOps will be initialized after jj object is created)
|
|
326
|
-
const jj = {
|
|
381
|
+
const jj = /** @type {any} */ ({
|
|
327
382
|
storage,
|
|
328
383
|
graph,
|
|
329
384
|
workingCopy,
|
|
330
385
|
oplog,
|
|
331
386
|
bookmarks,
|
|
332
387
|
revset,
|
|
333
|
-
|
|
388
|
+
tags, // TagStore instance (raw); public API is the `tag` namespace below
|
|
389
|
+
// NOTE: the raw ConflictModel is intentionally not exposed here because the
|
|
390
|
+
// public `conflicts` namespace (defined below) uses the same key. It remains
|
|
391
|
+
// accessible internally via the module closure.
|
|
392
|
+
conflictModel: conflicts, // v1.5: advanced access to the raw ConflictModel
|
|
334
393
|
workspaces,
|
|
335
394
|
userConfig,
|
|
336
|
-
mergeDrivers,
|
|
337
|
-
backgroundOps: null,
|
|
338
|
-
backend: gitBackend,
|
|
339
|
-
|
|
395
|
+
mergeDrivers, // v0.5: expose merge driver registry
|
|
396
|
+
backgroundOps: null, // Will be initialized below
|
|
397
|
+
backend: gitBackend, // Expose backend for advanced users
|
|
398
|
+
|
|
340
399
|
/**
|
|
341
400
|
* Initialize a new JJ repository
|
|
342
401
|
*
|
|
343
402
|
* NOTE: This is a convenience alias. For Git-backed repos, prefer jj.git.init()
|
|
344
403
|
* to match the JJ CLI semantics.
|
|
345
404
|
*/
|
|
346
|
-
async init(opts = {}) {
|
|
405
|
+
async init(/** @type {Record<string, any>} */ opts = {}) {
|
|
347
406
|
// If Git backend is available, delegate to jj.git.init()
|
|
348
407
|
if (gitBackend) {
|
|
349
408
|
return await jj.git.init(opts);
|
|
@@ -367,7 +426,7 @@ export async function createJJ(options) {
|
|
|
367
426
|
|
|
368
427
|
// Create root change
|
|
369
428
|
const rootChangeId = generateChangeId();
|
|
370
|
-
const user = userConfig.getUser();
|
|
429
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
371
430
|
const rootChange = {
|
|
372
431
|
changeId: rootChangeId,
|
|
373
432
|
commitId: '0000000000000000000000000000000000000000',
|
|
@@ -412,9 +471,7 @@ export async function createJJ(options) {
|
|
|
412
471
|
/**
|
|
413
472
|
* Write a file to the working copy
|
|
414
473
|
*
|
|
415
|
-
* @param {
|
|
416
|
-
* @param {string} args.path - File path relative to repo root
|
|
417
|
-
* @param {string|Uint8Array} args.data - File contents
|
|
474
|
+
* @param {Record<string, any>} args - Arguments
|
|
418
475
|
* @returns {Promise<Object>} File information including path, size, and mode
|
|
419
476
|
*/
|
|
420
477
|
async write(args) {
|
|
@@ -441,9 +498,7 @@ export async function createJJ(options) {
|
|
|
441
498
|
}
|
|
442
499
|
|
|
443
500
|
// Write the file
|
|
444
|
-
const data = typeof args.data === 'string'
|
|
445
|
-
? args.data
|
|
446
|
-
: args.data;
|
|
501
|
+
const data = typeof args.data === 'string' ? args.data : args.data;
|
|
447
502
|
await fs.promises.writeFile(fullPath, data, 'utf8');
|
|
448
503
|
|
|
449
504
|
// Track the file in working copy
|
|
@@ -468,10 +523,7 @@ export async function createJJ(options) {
|
|
|
468
523
|
/**
|
|
469
524
|
* Read a file from the working copy or from a specific change
|
|
470
525
|
*
|
|
471
|
-
* @param {
|
|
472
|
-
* @param {string} args.path - File path relative to repo root
|
|
473
|
-
* @param {string} [args.changeId] - Change ID to read from (defaults to working copy)
|
|
474
|
-
* @param {string} [args.encoding='utf-8'] - File encoding ('utf-8' or 'binary')
|
|
526
|
+
* @param {Record<string, any>} args - Arguments
|
|
475
527
|
* @returns {Promise<string|Uint8Array>} File contents
|
|
476
528
|
*/
|
|
477
529
|
async read(args) {
|
|
@@ -485,12 +537,15 @@ export async function createJJ(options) {
|
|
|
485
537
|
|
|
486
538
|
// Read from working copy if no changeId specified
|
|
487
539
|
if (!args.changeId) {
|
|
540
|
+
// v1.6: reflect out-of-band changes before reading the working copy.
|
|
541
|
+
await autoSnapshotWorkingCopy();
|
|
488
542
|
// Check sparse patterns
|
|
489
543
|
await workingCopy.load();
|
|
490
544
|
if (!workingCopy.matchesSparsePatterns(args.path)) {
|
|
491
545
|
throw new JJError('FILE_NOT_IN_SPARSE', `File ${args.path} not in sparse checkout`, {
|
|
492
546
|
path: args.path,
|
|
493
|
-
suggestion:
|
|
547
|
+
suggestion:
|
|
548
|
+
'Add pattern to sparse checkout: jj.sparse.add({ patterns: ["' + args.path + '"] })',
|
|
494
549
|
});
|
|
495
550
|
}
|
|
496
551
|
|
|
@@ -530,7 +585,11 @@ export async function createJJ(options) {
|
|
|
530
585
|
}
|
|
531
586
|
|
|
532
587
|
// Fallback: read from Git if we have a commitId
|
|
533
|
-
if (
|
|
588
|
+
if (
|
|
589
|
+
gitBackend &&
|
|
590
|
+
change.commitId &&
|
|
591
|
+
change.commitId !== '0000000000000000000000000000000000000000'
|
|
592
|
+
) {
|
|
534
593
|
try {
|
|
535
594
|
const git = (await import('isomorphic-git')).default;
|
|
536
595
|
const { blob } = await git.readBlob({
|
|
@@ -545,25 +604,33 @@ export async function createJJ(options) {
|
|
|
545
604
|
}
|
|
546
605
|
return blob;
|
|
547
606
|
} catch (error) {
|
|
548
|
-
throw new JJError(
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
607
|
+
throw new JJError(
|
|
608
|
+
'FILE_NOT_FOUND',
|
|
609
|
+
`File ${args.path} not found in change ${args.changeId}`,
|
|
610
|
+
{
|
|
611
|
+
path: args.path,
|
|
612
|
+
changeId: args.changeId,
|
|
613
|
+
originalError: error,
|
|
614
|
+
}
|
|
615
|
+
);
|
|
553
616
|
}
|
|
554
617
|
}
|
|
555
618
|
|
|
556
|
-
throw new JJError(
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
619
|
+
throw new JJError(
|
|
620
|
+
'FILE_NOT_FOUND',
|
|
621
|
+
`File ${args.path} not found in change ${args.changeId}`,
|
|
622
|
+
{
|
|
623
|
+
path: args.path,
|
|
624
|
+
changeId: args.changeId,
|
|
625
|
+
suggestion: 'File not in snapshot cache and no Git commit available',
|
|
626
|
+
}
|
|
627
|
+
);
|
|
561
628
|
},
|
|
562
629
|
|
|
563
630
|
/**
|
|
564
631
|
* Alias for read() - matches jj CLI 'cat' command
|
|
565
632
|
*
|
|
566
|
-
* @param {
|
|
633
|
+
* @param {Record<string, any>} args - Arguments (same as read())
|
|
567
634
|
* @returns {Promise<string|Uint8Array>} File contents
|
|
568
635
|
*/
|
|
569
636
|
async cat(args) {
|
|
@@ -573,14 +640,14 @@ export async function createJJ(options) {
|
|
|
573
640
|
/**
|
|
574
641
|
* List files in the working copy or in a specific change
|
|
575
642
|
*
|
|
576
|
-
* @param {
|
|
577
|
-
* @param {string} [args.changeId] - Change ID to list files from (defaults to working copy)
|
|
578
|
-
* @param {boolean} [args.recursive=false] - Include files in subdirectories (for future use)
|
|
643
|
+
* @param {Record<string, any>} [args={}] - Arguments
|
|
579
644
|
* @returns {Promise<Array<string>>} Array of file paths
|
|
580
645
|
*/
|
|
581
646
|
async listFiles(args = {}) {
|
|
582
647
|
// List working copy files if no changeId specified
|
|
583
648
|
if (!args.changeId) {
|
|
649
|
+
// v1.6: include files created on disk out-of-band.
|
|
650
|
+
await autoSnapshotWorkingCopy();
|
|
584
651
|
await workingCopy.load();
|
|
585
652
|
return await workingCopy.listFiles();
|
|
586
653
|
}
|
|
@@ -600,7 +667,11 @@ export async function createJJ(options) {
|
|
|
600
667
|
}
|
|
601
668
|
|
|
602
669
|
// Fallback: read from Git tree
|
|
603
|
-
if (
|
|
670
|
+
if (
|
|
671
|
+
gitBackend &&
|
|
672
|
+
change.commitId &&
|
|
673
|
+
change.commitId !== '0000000000000000000000000000000000000000'
|
|
674
|
+
) {
|
|
604
675
|
try {
|
|
605
676
|
const git = (await import('isomorphic-git')).default;
|
|
606
677
|
const { tree } = await git.readTree({
|
|
@@ -609,9 +680,7 @@ export async function createJJ(options) {
|
|
|
609
680
|
oid: change.commitId,
|
|
610
681
|
});
|
|
611
682
|
|
|
612
|
-
return tree
|
|
613
|
-
.filter(entry => entry.type === 'blob')
|
|
614
|
-
.map(entry => entry.path);
|
|
683
|
+
return tree.filter((entry) => entry.type === 'blob').map((entry) => entry.path);
|
|
615
684
|
} catch (error) {
|
|
616
685
|
throw new JJError('TREE_READ_FAILED', `Failed to read tree for change ${args.changeId}`, {
|
|
617
686
|
changeId: args.changeId,
|
|
@@ -628,10 +697,7 @@ export async function createJJ(options) {
|
|
|
628
697
|
*
|
|
629
698
|
* For large files, use streaming to avoid loading entire file into memory.
|
|
630
699
|
*
|
|
631
|
-
* @param {
|
|
632
|
-
* @param {string} args.path - File path relative to repo root
|
|
633
|
-
* @param {string} [args.changeId] - Change ID to read from (defaults to working copy)
|
|
634
|
-
* @param {string} [args.encoding] - Optional encoding ('utf-8', etc.)
|
|
700
|
+
* @param {Record<string, any>} args - Arguments
|
|
635
701
|
* @returns {Promise<ReadableStream>} Readable stream
|
|
636
702
|
* @throws {JJError} If not in Node.js environment or file not found
|
|
637
703
|
*/
|
|
@@ -653,7 +719,10 @@ export async function createJJ(options) {
|
|
|
653
719
|
if (!args.changeId) {
|
|
654
720
|
const fullPath = path.join(dir, args.path);
|
|
655
721
|
try {
|
|
656
|
-
const stream = fs.createReadStream(
|
|
722
|
+
const stream = fs.createReadStream(
|
|
723
|
+
fullPath,
|
|
724
|
+
args.encoding ? { encoding: args.encoding } : {}
|
|
725
|
+
);
|
|
657
726
|
return stream;
|
|
658
727
|
} catch (error) {
|
|
659
728
|
throw new JJError('FILE_NOT_FOUND', `File ${args.path} not found in working copy`, {
|
|
@@ -665,9 +734,14 @@ export async function createJJ(options) {
|
|
|
665
734
|
|
|
666
735
|
// Reading from a specific change - for historical data, fall back to read()
|
|
667
736
|
// Streaming from Git objects requires more complex implementation
|
|
668
|
-
throw new JJError(
|
|
669
|
-
|
|
670
|
-
|
|
737
|
+
throw new JJError(
|
|
738
|
+
'UNSUPPORTED_OPERATION',
|
|
739
|
+
'Streaming from historical changes not yet supported',
|
|
740
|
+
{
|
|
741
|
+
suggestion:
|
|
742
|
+
'Use read({ path, changeId }) for historical file access, or readStream({ path }) for working copy',
|
|
743
|
+
}
|
|
744
|
+
);
|
|
671
745
|
},
|
|
672
746
|
|
|
673
747
|
/**
|
|
@@ -675,9 +749,7 @@ export async function createJJ(options) {
|
|
|
675
749
|
*
|
|
676
750
|
* For large files, use streaming to avoid loading entire file into memory.
|
|
677
751
|
*
|
|
678
|
-
* @param {
|
|
679
|
-
* @param {string} args.path - File path relative to repo root
|
|
680
|
-
* @param {string} [args.encoding] - Optional encoding ('utf-8', etc.)
|
|
752
|
+
* @param {Record<string, any>} args - Arguments
|
|
681
753
|
* @returns {Promise<WritableStream>} Writable stream
|
|
682
754
|
* @throws {JJError} If not in Node.js environment
|
|
683
755
|
*/
|
|
@@ -706,7 +778,10 @@ export async function createJJ(options) {
|
|
|
706
778
|
}
|
|
707
779
|
|
|
708
780
|
// Create writable stream
|
|
709
|
-
const stream = fs.createWriteStream(
|
|
781
|
+
const stream = fs.createWriteStream(
|
|
782
|
+
fullPath,
|
|
783
|
+
args.encoding ? { encoding: args.encoding } : {}
|
|
784
|
+
);
|
|
710
785
|
|
|
711
786
|
// Track file when stream finishes
|
|
712
787
|
stream.on('finish', async () => {
|
|
@@ -748,20 +823,13 @@ export async function createJJ(options) {
|
|
|
748
823
|
* - Presence of only `from` and `to` → file operation
|
|
749
824
|
* - Change IDs are 32-character hex strings; file paths are not
|
|
750
825
|
*
|
|
751
|
-
* @param {
|
|
752
|
-
* @param {string} [args.from] - Source path or change ID
|
|
753
|
-
* @param {string} [args.to] - Destination path or change ID
|
|
754
|
-
* @param {string} [args.changeId] - Change ID to move (for history operations)
|
|
755
|
-
* @param {string} [args.newParent] - New parent change ID (for history operations)
|
|
756
|
-
* @param {string[]} [args.paths] - Paths to move between changes (for history operations)
|
|
826
|
+
* @param {Record<string, any>} args - Arguments
|
|
757
827
|
* @returns {Promise<Object>} Returns change object for history operations, file info for file operations
|
|
758
828
|
*/
|
|
759
829
|
/**
|
|
760
830
|
* Move a file in the working copy
|
|
761
831
|
*
|
|
762
|
-
* @param {
|
|
763
|
-
* @param {string} args.from - Source file path
|
|
764
|
-
* @param {string} args.to - Destination file path
|
|
832
|
+
* @param {Record<string, any>} args - Move file arguments
|
|
765
833
|
* @returns {Promise<Object>} Operation result including from, to paths and file stats
|
|
766
834
|
*/
|
|
767
835
|
async moveFile(args) {
|
|
@@ -821,10 +889,14 @@ export async function createJJ(options) {
|
|
|
821
889
|
try {
|
|
822
890
|
await fs.promises.mkdir(toDir, { recursive: true });
|
|
823
891
|
} catch (error) {
|
|
824
|
-
throw new JJError(
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
892
|
+
throw new JJError(
|
|
893
|
+
'FILE_SYSTEM_ERROR',
|
|
894
|
+
`Failed to create destination directory: ${error.message}`,
|
|
895
|
+
{
|
|
896
|
+
path: toDir,
|
|
897
|
+
error: error.message,
|
|
898
|
+
}
|
|
899
|
+
);
|
|
828
900
|
}
|
|
829
901
|
|
|
830
902
|
// Move the file
|
|
@@ -860,9 +932,7 @@ export async function createJJ(options) {
|
|
|
860
932
|
/**
|
|
861
933
|
* Move (rebase) a change to a new parent
|
|
862
934
|
*
|
|
863
|
-
* @param {
|
|
864
|
-
* @param {string} args.changeId - Change ID to move (also accepts 'from' or 'source')
|
|
865
|
-
* @param {string} args.newParent - New parent change ID (also accepts 'to' or 'destination')
|
|
935
|
+
* @param {Record<string, any>} args - Move change arguments (changeId/from/source, newParent/to/destination)
|
|
866
936
|
* @returns {Promise<Object>} Updated change object
|
|
867
937
|
*/
|
|
868
938
|
async moveChange(args) {
|
|
@@ -896,10 +966,14 @@ export async function createJJ(options) {
|
|
|
896
966
|
}
|
|
897
967
|
|
|
898
968
|
if (!changeIdPattern.test(newParent)) {
|
|
899
|
-
throw new JJError(
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
969
|
+
throw new JJError(
|
|
970
|
+
'INVALID_CHANGE_ID',
|
|
971
|
+
`Invalid new parent change ID format: ${newParent}`,
|
|
972
|
+
{
|
|
973
|
+
suggestion: 'Change IDs must be 32-character hex strings',
|
|
974
|
+
provided: newParent,
|
|
975
|
+
}
|
|
976
|
+
);
|
|
903
977
|
}
|
|
904
978
|
|
|
905
979
|
const change = await graph.getChange(changeId);
|
|
@@ -955,11 +1029,13 @@ export async function createJJ(options) {
|
|
|
955
1029
|
/**
|
|
956
1030
|
* @deprecated Use moveFile() for files or moveChange() for history operations
|
|
957
1031
|
* Move operation - delegates to moveFile or moveChange based on arguments
|
|
1032
|
+
* @param {Record<string, any>} args
|
|
958
1033
|
*/
|
|
959
1034
|
async move(args) {
|
|
960
1035
|
if (!args || typeof args !== 'object') {
|
|
961
1036
|
throw new JJError('INVALID_ARGUMENT', 'Missing or invalid arguments', {
|
|
962
|
-
suggestion:
|
|
1037
|
+
suggestion:
|
|
1038
|
+
'Use moveFile({ from, to }) for files or moveChange({ changeId, newParent }) for history',
|
|
963
1039
|
});
|
|
964
1040
|
}
|
|
965
1041
|
|
|
@@ -977,7 +1053,7 @@ export async function createJJ(options) {
|
|
|
977
1053
|
* Detect if move operation is for history (rebase) or files
|
|
978
1054
|
*
|
|
979
1055
|
* @private
|
|
980
|
-
* @param {
|
|
1056
|
+
* @param {Record<string, any>} args - Move arguments
|
|
981
1057
|
* @returns {boolean} True if history operation, false if file operation
|
|
982
1058
|
*/
|
|
983
1059
|
_detectHistoryOperation(args) {
|
|
@@ -998,11 +1074,15 @@ export async function createJJ(options) {
|
|
|
998
1074
|
// Users should use explicit { changeId, newParent } for clarity
|
|
999
1075
|
if (fromLooksLikeChangeId && toLooksLikeChangeId) {
|
|
1000
1076
|
// This is ambiguous - throw a helpful error
|
|
1001
|
-
throw new JJError(
|
|
1002
|
-
'
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1077
|
+
throw new JJError(
|
|
1078
|
+
'AMBIGUOUS_OPERATION',
|
|
1079
|
+
'Ambiguous move operation: arguments look like change IDs',
|
|
1080
|
+
{
|
|
1081
|
+
suggestion:
|
|
1082
|
+
"Use { changeId, newParent } for history operations or ensure file paths don't match change ID pattern",
|
|
1083
|
+
args,
|
|
1084
|
+
}
|
|
1085
|
+
);
|
|
1006
1086
|
}
|
|
1007
1087
|
|
|
1008
1088
|
return false; // Treat as file operation
|
|
@@ -1014,8 +1094,7 @@ export async function createJJ(options) {
|
|
|
1014
1094
|
/**
|
|
1015
1095
|
* Remove a file from the working copy
|
|
1016
1096
|
*
|
|
1017
|
-
* @param {
|
|
1018
|
-
* @param {string} args.path - File path to remove
|
|
1097
|
+
* @param {Record<string, any>} args - Arguments
|
|
1019
1098
|
* @returns {Promise<Object>} Information about the removed file
|
|
1020
1099
|
*/
|
|
1021
1100
|
async remove(args) {
|
|
@@ -1048,16 +1127,23 @@ export async function createJJ(options) {
|
|
|
1048
1127
|
|
|
1049
1128
|
/**
|
|
1050
1129
|
* Describe the working copy change
|
|
1130
|
+
* @param {Record<string, any>} [args]
|
|
1051
1131
|
*/
|
|
1052
1132
|
async describe(args = {}) {
|
|
1053
1133
|
await graph.load();
|
|
1054
1134
|
await workingCopy.load();
|
|
1055
1135
|
await userConfig.load();
|
|
1056
1136
|
|
|
1137
|
+
// v1.6: pick up files created/changed on disk out-of-band so they are
|
|
1138
|
+
// captured by this describe (unless a specific revision is targeted).
|
|
1139
|
+
if (!args.revision) {
|
|
1140
|
+
await autoSnapshotWorkingCopy();
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1057
1143
|
// Support describing specific revision (not just working copy)
|
|
1058
1144
|
const targetChangeId = args.revision || workingCopy.getCurrentChangeId();
|
|
1059
1145
|
const change = await graph.getChange(targetChangeId);
|
|
1060
|
-
const user = userConfig.getUser();
|
|
1146
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
1061
1147
|
|
|
1062
1148
|
if (!change) {
|
|
1063
1149
|
throw new JJError('CHANGE_NOT_FOUND', `Change ${targetChangeId} not found`, {
|
|
@@ -1069,6 +1155,11 @@ export async function createJJ(options) {
|
|
|
1069
1155
|
const isWorkingCopy = targetChangeId === workingCopy.getCurrentChangeId();
|
|
1070
1156
|
const fileSnapshotBefore = isWorkingCopy ? await snapshotFilesystem() : null;
|
|
1071
1157
|
|
|
1158
|
+
// Snapshot the change record itself from BEFORE this operation mutates
|
|
1159
|
+
// it in place, so undo() can restore ChangeGraph content (not just
|
|
1160
|
+
// on-disk files/working-copy pointer) — mirrors fileSnapshotBefore above.
|
|
1161
|
+
const changeSnapshotBefore = structuredClone(change);
|
|
1162
|
+
|
|
1072
1163
|
// Dispatch change:updating event (preventable)
|
|
1073
1164
|
await dispatchEventAsync(jj, 'change:updating', {
|
|
1074
1165
|
operation: 'describe',
|
|
@@ -1093,6 +1184,7 @@ export async function createJJ(options) {
|
|
|
1093
1184
|
if (isWorkingCopy && !args.preserveSnapshot) {
|
|
1094
1185
|
// Snapshot current file contents for conflict detection
|
|
1095
1186
|
// This allows us to load file contents during merge/rebase
|
|
1187
|
+
/** @type {Record<string, any>} */
|
|
1096
1188
|
const fileSnapshot = {};
|
|
1097
1189
|
const trackedFiles = await workingCopy.listFiles();
|
|
1098
1190
|
|
|
@@ -1124,7 +1216,11 @@ export async function createJJ(options) {
|
|
|
1124
1216
|
throw new JJError(
|
|
1125
1217
|
'SNAPSHOT_FILE_FAILED',
|
|
1126
1218
|
`Could not snapshot file ${filePath}: ${error.message}`,
|
|
1127
|
-
{
|
|
1219
|
+
{
|
|
1220
|
+
filePath,
|
|
1221
|
+
originalError: error.message,
|
|
1222
|
+
suggestion: 'File may be binary, deleted, or inaccessible',
|
|
1223
|
+
}
|
|
1128
1224
|
);
|
|
1129
1225
|
}
|
|
1130
1226
|
}
|
|
@@ -1144,6 +1240,10 @@ export async function createJJ(options) {
|
|
|
1144
1240
|
},
|
|
1145
1241
|
description: `describe change ${targetChangeId.slice(0, 8)}`,
|
|
1146
1242
|
parents: [],
|
|
1243
|
+
// Snapshot of the affected ChangeGraph node from BEFORE this
|
|
1244
|
+
// operation ran, so undo() can restore in-place content mutations
|
|
1245
|
+
// (e.g. the description change below) — not just files/pointers.
|
|
1246
|
+
changeSnapshot: { [targetChangeId]: changeSnapshotBefore },
|
|
1147
1247
|
view: {
|
|
1148
1248
|
bookmarks: {},
|
|
1149
1249
|
remoteBookmarks: {},
|
|
@@ -1154,12 +1254,17 @@ export async function createJJ(options) {
|
|
|
1154
1254
|
});
|
|
1155
1255
|
|
|
1156
1256
|
// Dispatch change:updated event (informational)
|
|
1157
|
-
await dispatchEventAsync(
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1257
|
+
await dispatchEventAsync(
|
|
1258
|
+
jj,
|
|
1259
|
+
'change:updated',
|
|
1260
|
+
{
|
|
1261
|
+
operation: 'describe',
|
|
1262
|
+
changeId: targetChangeId,
|
|
1263
|
+
change,
|
|
1264
|
+
timestamp: new Date().toISOString(),
|
|
1265
|
+
},
|
|
1266
|
+
{ cancelable: false }
|
|
1267
|
+
);
|
|
1163
1268
|
|
|
1164
1269
|
return change;
|
|
1165
1270
|
},
|
|
@@ -1170,18 +1275,7 @@ export async function createJJ(options) {
|
|
|
1170
1275
|
* Allows updating author, committer, description, and optionally regenerating the change ID.
|
|
1171
1276
|
* The file content and snapshot remain unchanged.
|
|
1172
1277
|
*
|
|
1173
|
-
* @param {
|
|
1174
|
-
* @param {string} [args.revision] - Change ID to edit (defaults to working copy @)
|
|
1175
|
-
* @param {string} [args.change] - Alias for revision
|
|
1176
|
-
* @param {string} [args.description] - Update the change description
|
|
1177
|
-
* @param {string} [args.message] - Alias for description
|
|
1178
|
-
* @param {Object} [args.author] - Author information to update
|
|
1179
|
-
* @param {string} [args.author.name] - Author name
|
|
1180
|
-
* @param {string} [args.author.email] - Author email
|
|
1181
|
-
* @param {Object} [args.committer] - Committer information to update
|
|
1182
|
-
* @param {string} [args.committer.name] - Committer name
|
|
1183
|
-
* @param {string} [args.committer.email] - Committer email
|
|
1184
|
-
* @param {boolean} [args.resetChangeId=false] - Generate new change ID
|
|
1278
|
+
* @param {Record<string, any>} [args={}] - Arguments
|
|
1185
1279
|
* @returns {Promise<Object>} Updated change object
|
|
1186
1280
|
*/
|
|
1187
1281
|
async metaedit(args = {}) {
|
|
@@ -1202,7 +1296,7 @@ export async function createJJ(options) {
|
|
|
1202
1296
|
// Determine which change to edit
|
|
1203
1297
|
const changeId = args.revision || workingCopy.getCurrentChangeId();
|
|
1204
1298
|
const change = await graph.getChange(changeId);
|
|
1205
|
-
const user = userConfig.getUser();
|
|
1299
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
1206
1300
|
|
|
1207
1301
|
if (!change) {
|
|
1208
1302
|
throw new JJError('CHANGE_NOT_FOUND', `Change ${changeId} not found`);
|
|
@@ -1215,17 +1309,13 @@ export async function createJJ(options) {
|
|
|
1215
1309
|
const hasDescription = args.description !== undefined;
|
|
1216
1310
|
|
|
1217
1311
|
if (!hasAuthor && !hasCommitter && !hasResetChangeId && !hasDescription) {
|
|
1218
|
-
throw new JJError(
|
|
1219
|
-
'
|
|
1220
|
-
|
|
1221
|
-
{
|
|
1222
|
-
suggestion: 'Provide author, committer, description, or resetChangeId: true',
|
|
1223
|
-
}
|
|
1224
|
-
);
|
|
1312
|
+
throw new JJError('INVALID_ARGUMENT', 'No metadata provided to update', {
|
|
1313
|
+
suggestion: 'Provide author, committer, description, or resetChangeId: true',
|
|
1314
|
+
});
|
|
1225
1315
|
}
|
|
1226
1316
|
|
|
1227
1317
|
// Store old change ID for potential regeneration
|
|
1228
|
-
const
|
|
1318
|
+
const _oldChangeId = change.changeId;
|
|
1229
1319
|
|
|
1230
1320
|
// Update author if provided
|
|
1231
1321
|
if (args.author) {
|
|
@@ -1276,15 +1366,11 @@ export async function createJJ(options) {
|
|
|
1276
1366
|
// - Updating all children to point to new change ID
|
|
1277
1367
|
// - Updating all bookmarks pointing to old change ID
|
|
1278
1368
|
// - Updating operation log references
|
|
1279
|
-
throw new JJError(
|
|
1280
|
-
'
|
|
1281
|
-
'
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
reason: 'Requires complex graph manipulation to update all references',
|
|
1285
|
-
suggestion: 'Create a new change with desired metadata instead',
|
|
1286
|
-
}
|
|
1287
|
-
);
|
|
1369
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'resetChangeId is not yet implemented', {
|
|
1370
|
+
feature: 'metaedit resetChangeId',
|
|
1371
|
+
reason: 'Requires complex graph manipulation to update all references',
|
|
1372
|
+
suggestion: 'Create a new change with desired metadata instead',
|
|
1373
|
+
});
|
|
1288
1374
|
}
|
|
1289
1375
|
|
|
1290
1376
|
// Update the existing change
|
|
@@ -1310,12 +1396,7 @@ export async function createJJ(options) {
|
|
|
1310
1396
|
/**
|
|
1311
1397
|
* Create a new change
|
|
1312
1398
|
*
|
|
1313
|
-
* @param {
|
|
1314
|
-
* @param {string} [args.message] - Initial description for the new change
|
|
1315
|
-
* @param {string|string[]} [args.parents] - Parent change ID(s), supports merge commits
|
|
1316
|
-
* @param {string} [args.from] - Single parent (backward compat)
|
|
1317
|
-
* @param {string} [args.insertAfter] - Insert after this change
|
|
1318
|
-
* @param {string} [args.insertBefore] - Insert before this change
|
|
1399
|
+
* @param {Record<string, any>} [args={}] - Arguments
|
|
1319
1400
|
* @returns {Promise<Object>} The new change object including changeId, description, parents, author, and timestamp
|
|
1320
1401
|
*/
|
|
1321
1402
|
async new(args = {}) {
|
|
@@ -1357,7 +1438,7 @@ export async function createJJ(options) {
|
|
|
1357
1438
|
}
|
|
1358
1439
|
|
|
1359
1440
|
const newChangeId = generateChangeId();
|
|
1360
|
-
const user = userConfig.getUser();
|
|
1441
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
1361
1442
|
|
|
1362
1443
|
// Initialize with parent's file snapshot
|
|
1363
1444
|
let initialSnapshot = {};
|
|
@@ -1398,7 +1479,7 @@ export async function createJJ(options) {
|
|
|
1398
1479
|
timestamp: new Date().toISOString(),
|
|
1399
1480
|
});
|
|
1400
1481
|
|
|
1401
|
-
await graph.addChange(newChange);
|
|
1482
|
+
await graph.addChange(newChange); // Middleware will sync to Git
|
|
1402
1483
|
|
|
1403
1484
|
// Handle insertBefore: rebase target to have new change as parent
|
|
1404
1485
|
if (args.insertBefore) {
|
|
@@ -1429,17 +1510,22 @@ export async function createJJ(options) {
|
|
|
1429
1510
|
});
|
|
1430
1511
|
|
|
1431
1512
|
// Dispatch change:created event (informational)
|
|
1432
|
-
await dispatchEventAsync(
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1513
|
+
await dispatchEventAsync(
|
|
1514
|
+
jj,
|
|
1515
|
+
'change:created',
|
|
1516
|
+
{
|
|
1517
|
+
operation: 'new',
|
|
1518
|
+
changeId: newChangeId,
|
|
1519
|
+
parents: parents,
|
|
1520
|
+
change: newChange,
|
|
1521
|
+
timestamp: new Date().toISOString(),
|
|
1522
|
+
},
|
|
1523
|
+
{ cancelable: false }
|
|
1524
|
+
);
|
|
1439
1525
|
|
|
1440
1526
|
return newChange;
|
|
1441
1527
|
},
|
|
1442
|
-
|
|
1528
|
+
|
|
1443
1529
|
/**
|
|
1444
1530
|
* Get working copy status
|
|
1445
1531
|
*/
|
|
@@ -1448,21 +1534,57 @@ export async function createJJ(options) {
|
|
|
1448
1534
|
await workingCopy.load();
|
|
1449
1535
|
await conflicts.load();
|
|
1450
1536
|
|
|
1537
|
+
// Dirty tracked files (computed before reconciling so modifications to
|
|
1538
|
+
// already-tracked files are still reported).
|
|
1539
|
+
const modified = await workingCopy.getModifiedFiles();
|
|
1540
|
+
|
|
1541
|
+
// v1.6: snapshot the working directory so files added/removed on disk
|
|
1542
|
+
// out-of-band show up in status.
|
|
1543
|
+
const snap = await autoSnapshotWorkingCopy();
|
|
1544
|
+
|
|
1451
1545
|
const currentChangeId = workingCopy.getCurrentChangeId();
|
|
1452
1546
|
const change = await graph.getChange(currentChangeId);
|
|
1453
|
-
const modified = await workingCopy.getModifiedFiles();
|
|
1454
1547
|
// Only show unresolved conflicts in status
|
|
1455
1548
|
const activeConflicts = conflicts.listConflicts({ resolved: false });
|
|
1456
1549
|
|
|
1457
1550
|
return {
|
|
1458
1551
|
workingCopy: change,
|
|
1459
1552
|
modified,
|
|
1460
|
-
added: [],
|
|
1461
|
-
removed: [],
|
|
1553
|
+
added: snap ? snap.added : [],
|
|
1554
|
+
removed: snap ? snap.deleted : [],
|
|
1462
1555
|
conflicts: activeConflicts,
|
|
1463
1556
|
};
|
|
1464
1557
|
},
|
|
1465
1558
|
|
|
1559
|
+
/**
|
|
1560
|
+
* Explicitly snapshot the working directory (v1.6).
|
|
1561
|
+
*
|
|
1562
|
+
* Walks the working copy on disk and reconciles tracked file state and the
|
|
1563
|
+
* working-copy change's content, picking up files created/modified/deleted
|
|
1564
|
+
* outside of jj.write() (editor, shell, git checkout). This runs
|
|
1565
|
+
* automatically before status/describe/diff/read/file.* unless the repo was
|
|
1566
|
+
* created with `{ autoSnapshot: false }`; call this to trigger it manually.
|
|
1567
|
+
*
|
|
1568
|
+
* @returns {Promise<{added: string[], modified: string[], deleted: string[]}>}
|
|
1569
|
+
*/
|
|
1570
|
+
async snapshot() {
|
|
1571
|
+
await graph.load();
|
|
1572
|
+
await workingCopy.load();
|
|
1573
|
+
const result = await workingCopy.snapshot();
|
|
1574
|
+
// Refresh the working-copy change's content snapshot from disk.
|
|
1575
|
+
try {
|
|
1576
|
+
const wcId = workingCopy.getCurrentChangeId();
|
|
1577
|
+
const change = await graph.getChange(wcId);
|
|
1578
|
+
if (change) {
|
|
1579
|
+
change.fileSnapshot = await snapshotFilesystem();
|
|
1580
|
+
await graph.updateChange(change);
|
|
1581
|
+
}
|
|
1582
|
+
} catch {
|
|
1583
|
+
// Best-effort content refresh.
|
|
1584
|
+
}
|
|
1585
|
+
return result;
|
|
1586
|
+
},
|
|
1587
|
+
|
|
1466
1588
|
/**
|
|
1467
1589
|
* Get repository statistics and analytics
|
|
1468
1590
|
*
|
|
@@ -1477,9 +1599,10 @@ export async function createJJ(options) {
|
|
|
1477
1599
|
|
|
1478
1600
|
const all = graph.getAll();
|
|
1479
1601
|
const ops = await oplog.list();
|
|
1480
|
-
const currentUser = userConfig.getUser();
|
|
1602
|
+
const currentUser = /** @type {any} */ (userConfig.getUser());
|
|
1481
1603
|
|
|
1482
1604
|
// Count by author
|
|
1605
|
+
/** @type {Record<string, any>} */
|
|
1483
1606
|
const authorCounts = {};
|
|
1484
1607
|
for (const change of all) {
|
|
1485
1608
|
if (!change.abandoned && change.author) {
|
|
@@ -1499,26 +1622,30 @@ export async function createJJ(options) {
|
|
|
1499
1622
|
}
|
|
1500
1623
|
|
|
1501
1624
|
// Count merge commits
|
|
1502
|
-
const mergeCommits = all.filter(
|
|
1625
|
+
const mergeCommits = all.filter(
|
|
1626
|
+
(/** @type {any} */ c) => c.parents && c.parents.length > 1
|
|
1627
|
+
).length;
|
|
1503
1628
|
|
|
1504
1629
|
// Count empty commits
|
|
1505
|
-
const emptyCommits = all.filter(
|
|
1506
|
-
c.tree === '0000000000000000000000000000000000000000'
|
|
1630
|
+
const emptyCommits = all.filter(
|
|
1631
|
+
(/** @type {any} */ c) => c.tree === '0000000000000000000000000000000000000000'
|
|
1507
1632
|
).length;
|
|
1508
1633
|
|
|
1509
1634
|
// Count abandoned
|
|
1510
|
-
const abandonedCount = all.filter(c => c.abandoned).length;
|
|
1635
|
+
const abandonedCount = all.filter((/** @type {any} */ c) => c.abandoned).length;
|
|
1511
1636
|
|
|
1512
1637
|
// My commits
|
|
1513
|
-
const myCommits = all.filter(
|
|
1514
|
-
|
|
1515
|
-
|
|
1638
|
+
const myCommits = all.filter(
|
|
1639
|
+
(/** @type {any} */ c) =>
|
|
1640
|
+
c.author &&
|
|
1641
|
+
!c.abandoned &&
|
|
1642
|
+
(c.author.email === currentUser.email || c.author.name === currentUser.name)
|
|
1516
1643
|
).length;
|
|
1517
1644
|
|
|
1518
1645
|
// Get bookmark statistics
|
|
1519
1646
|
const allBookmarks = await bookmarks.list();
|
|
1520
|
-
const localBookmarks = allBookmarks.filter(b => !b.remote);
|
|
1521
|
-
const remoteBookmarks = allBookmarks.filter(b => b.remote);
|
|
1647
|
+
const localBookmarks = allBookmarks.filter((b) => !b.remote);
|
|
1648
|
+
const remoteBookmarks = allBookmarks.filter((b) => b.remote);
|
|
1522
1649
|
|
|
1523
1650
|
return {
|
|
1524
1651
|
changes: {
|
|
@@ -1537,7 +1664,7 @@ export async function createJJ(options) {
|
|
|
1537
1664
|
files: {
|
|
1538
1665
|
total: allFiles.size,
|
|
1539
1666
|
list: Array.from(allFiles).sort(),
|
|
1540
|
-
byExtension: {},
|
|
1667
|
+
byExtension: {}, // Add file extension breakdown
|
|
1541
1668
|
},
|
|
1542
1669
|
bookmarks: {
|
|
1543
1670
|
total: allBookmarks.length,
|
|
@@ -1558,11 +1685,8 @@ export async function createJJ(options) {
|
|
|
1558
1685
|
/**
|
|
1559
1686
|
* View change history
|
|
1560
1687
|
*
|
|
1561
|
-
* @param {
|
|
1562
|
-
* @
|
|
1563
|
-
* @param {number} [args.limit] - Maximum number of changes to return
|
|
1564
|
-
* @param {boolean} [args.count=false] - If true, return count instead of changes (v0.36.0)
|
|
1565
|
-
* @returns {Promise<Array|number>} Array of changes or count if args.count is true
|
|
1688
|
+
* @param {Record<string, any>} args - Arguments
|
|
1689
|
+
* @returns {Promise<Array<any>|number>} Array of changes or count if args.count is true
|
|
1566
1690
|
*/
|
|
1567
1691
|
async log(args = {}) {
|
|
1568
1692
|
await graph.load();
|
|
@@ -1591,7 +1715,7 @@ export async function createJJ(options) {
|
|
|
1591
1715
|
const aIsParentOfB = b.parents && b.parents.includes(a.changeId);
|
|
1592
1716
|
const bIsParentOfA = a.parents && a.parents.includes(b.changeId);
|
|
1593
1717
|
|
|
1594
|
-
if (aIsParentOfB) return 1;
|
|
1718
|
+
if (aIsParentOfB) return 1; // a is parent of b, so b comes first
|
|
1595
1719
|
if (bIsParentOfA) return -1; // b is parent of a, so a comes first
|
|
1596
1720
|
|
|
1597
1721
|
// Secondary: Sort by timestamp descending (newest first)
|
|
@@ -1611,14 +1735,13 @@ export async function createJJ(options) {
|
|
|
1611
1735
|
/**
|
|
1612
1736
|
* Show detailed information about a specific change
|
|
1613
1737
|
*
|
|
1614
|
-
* @param {
|
|
1615
|
-
* @param {string} args.change - Change ID or revset to show
|
|
1738
|
+
* @param {Record<string, any>} args - Arguments
|
|
1616
1739
|
* @returns {Promise<Object>} Detailed change information
|
|
1617
1740
|
*/
|
|
1618
1741
|
async show(args) {
|
|
1619
1742
|
if (!args || !args.change) {
|
|
1620
1743
|
throw new JJError('INVALID_ARGUMENT', 'Missing change argument', {
|
|
1621
|
-
suggestion: 'Provide { change: changeId }'
|
|
1744
|
+
suggestion: 'Provide { change: changeId }',
|
|
1622
1745
|
});
|
|
1623
1746
|
}
|
|
1624
1747
|
|
|
@@ -1633,11 +1756,28 @@ export async function createJJ(options) {
|
|
|
1633
1756
|
// Try to resolve as revset
|
|
1634
1757
|
try {
|
|
1635
1758
|
const changes = await revset.evaluate(args.change);
|
|
1759
|
+
if (changes.length > 1) {
|
|
1760
|
+
// An id-prefix (or other) revset that matches more than one
|
|
1761
|
+
// change must not silently pick a "winner" — see issue #13.
|
|
1762
|
+
throw new JJError(
|
|
1763
|
+
'AMBIGUOUS_ID',
|
|
1764
|
+
`"${args.change}" is ambiguous: it resolves to ${changes.length} changes (${changes.join(', ')})`,
|
|
1765
|
+
{
|
|
1766
|
+
expression: args.change,
|
|
1767
|
+
matches: changes,
|
|
1768
|
+
suggestion: 'Use a longer prefix or an exact change ID to disambiguate',
|
|
1769
|
+
}
|
|
1770
|
+
);
|
|
1771
|
+
}
|
|
1636
1772
|
if (changes.length > 0) {
|
|
1637
1773
|
changeId = changes[0];
|
|
1638
1774
|
}
|
|
1639
|
-
} catch {
|
|
1640
|
-
|
|
1775
|
+
} catch (error) {
|
|
1776
|
+
if (error instanceof JJError && error.code === 'AMBIGUOUS_ID') {
|
|
1777
|
+
throw error;
|
|
1778
|
+
}
|
|
1779
|
+
// Not a valid revset expression - use args.change as-is (e.g. a
|
|
1780
|
+
// literal change ID that revset parsing rejected).
|
|
1641
1781
|
}
|
|
1642
1782
|
}
|
|
1643
1783
|
|
|
@@ -1654,23 +1794,23 @@ export async function createJJ(options) {
|
|
|
1654
1794
|
// Find bookmarks pointing to this change
|
|
1655
1795
|
const allBookmarks = await bookmarks.list();
|
|
1656
1796
|
const changeBookmarks = allBookmarks
|
|
1657
|
-
.filter(b => b.
|
|
1658
|
-
.map(b => b.name);
|
|
1797
|
+
.filter((b) => b.changeId === change.changeId)
|
|
1798
|
+
.map((b) => b.name);
|
|
1659
1799
|
|
|
1660
1800
|
// Find operations that modified this change
|
|
1661
1801
|
const ops = await oplog.list();
|
|
1662
|
-
const relatedOps = ops.filter(
|
|
1663
|
-
op
|
|
1664
|
-
op.view
|
|
1665
|
-
(op.view.
|
|
1666
|
-
|
|
1802
|
+
const relatedOps = ops.filter(
|
|
1803
|
+
(op) =>
|
|
1804
|
+
op.view &&
|
|
1805
|
+
(op.view.workingCopy === change.changeId ||
|
|
1806
|
+
(op.view.heads && op.view.heads.includes(change.changeId)))
|
|
1667
1807
|
);
|
|
1668
1808
|
|
|
1669
1809
|
return {
|
|
1670
1810
|
...change,
|
|
1671
1811
|
children,
|
|
1672
1812
|
bookmarks: changeBookmarks,
|
|
1673
|
-
operations: relatedOps.map(op => ({
|
|
1813
|
+
operations: relatedOps.map((op) => ({
|
|
1674
1814
|
id: op.id,
|
|
1675
1815
|
timestamp: op.timestamp,
|
|
1676
1816
|
description: op.description,
|
|
@@ -1681,8 +1821,7 @@ export async function createJJ(options) {
|
|
|
1681
1821
|
/**
|
|
1682
1822
|
* Amend the working copy change
|
|
1683
1823
|
*
|
|
1684
|
-
* @param {
|
|
1685
|
-
* @param {string} [args.message] - New description message
|
|
1824
|
+
* @param {Record<string, any>} args - Arguments
|
|
1686
1825
|
* @returns {Promise<Object>} Updated change
|
|
1687
1826
|
*/
|
|
1688
1827
|
async amend(args = {}) {
|
|
@@ -1693,10 +1832,7 @@ export async function createJJ(options) {
|
|
|
1693
1832
|
/**
|
|
1694
1833
|
* Convenience: describe current change and create new one (jj commit)
|
|
1695
1834
|
*
|
|
1696
|
-
* @param {
|
|
1697
|
-
* @param {string} [args.message] - Description for current change
|
|
1698
|
-
* @param {Object} [args.author] - Author for current change
|
|
1699
|
-
* @param {Object} [args.metadata] - Metadata to attach to the change
|
|
1835
|
+
* @param {Record<string, any>} [args={}] - Arguments
|
|
1700
1836
|
* @returns {Promise<Object>} The newly created change
|
|
1701
1837
|
*/
|
|
1702
1838
|
async commit(args = {}) {
|
|
@@ -1704,19 +1840,18 @@ export async function createJJ(options) {
|
|
|
1704
1840
|
message: args.message,
|
|
1705
1841
|
author: args.author,
|
|
1706
1842
|
metadata: args.metadata,
|
|
1707
|
-
preserveSnapshot: args.preserveSnapshot
|
|
1843
|
+
preserveSnapshot: args.preserveSnapshot,
|
|
1708
1844
|
});
|
|
1709
1845
|
return await this.new({
|
|
1710
1846
|
message: args.nextMessage,
|
|
1711
|
-
preserveSnapshot: args.preserveSnapshot
|
|
1847
|
+
preserveSnapshot: args.preserveSnapshot,
|
|
1712
1848
|
});
|
|
1713
1849
|
},
|
|
1714
1850
|
|
|
1715
1851
|
/**
|
|
1716
1852
|
* Edit a specific change (make it the working copy)
|
|
1717
1853
|
*
|
|
1718
|
-
* @param {
|
|
1719
|
-
* @param {string} args.changeId - Change ID to edit
|
|
1854
|
+
* @param {Record<string, any>} args - Arguments
|
|
1720
1855
|
* @returns {Promise<Object>} Information about the edited change including changeId, description, and file count
|
|
1721
1856
|
*/
|
|
1722
1857
|
async edit(args) {
|
|
@@ -1736,7 +1871,7 @@ export async function createJJ(options) {
|
|
|
1736
1871
|
await userConfig.load();
|
|
1737
1872
|
|
|
1738
1873
|
const change = await graph.getChange(args.changeId);
|
|
1739
|
-
const user = userConfig.getUser();
|
|
1874
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
1740
1875
|
if (!change) {
|
|
1741
1876
|
throw new JJError('CHANGE_NOT_FOUND', `Change ${args.changeId} not found`, {
|
|
1742
1877
|
changeId: args.changeId,
|
|
@@ -1841,13 +1976,18 @@ export async function createJJ(options) {
|
|
|
1841
1976
|
});
|
|
1842
1977
|
|
|
1843
1978
|
// Dispatch workingcopy:switched event (informational)
|
|
1844
|
-
await dispatchEventAsync(
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1979
|
+
await dispatchEventAsync(
|
|
1980
|
+
jj,
|
|
1981
|
+
'workingcopy:switched',
|
|
1982
|
+
{
|
|
1983
|
+
operation: 'edit',
|
|
1984
|
+
fromChangeId: previousChangeId,
|
|
1985
|
+
toChangeId: args.changeId,
|
|
1986
|
+
change,
|
|
1987
|
+
timestamp: new Date().toISOString(),
|
|
1988
|
+
},
|
|
1989
|
+
{ cancelable: false }
|
|
1990
|
+
);
|
|
1851
1991
|
|
|
1852
1992
|
// Return information about the edited change
|
|
1853
1993
|
return {
|
|
@@ -1858,7 +1998,7 @@ export async function createJJ(options) {
|
|
|
1858
1998
|
timestamp: change.timestamp,
|
|
1859
1999
|
};
|
|
1860
2000
|
},
|
|
1861
|
-
|
|
2001
|
+
|
|
1862
2002
|
/**
|
|
1863
2003
|
* Undo last operation
|
|
1864
2004
|
*
|
|
@@ -1875,11 +2015,26 @@ export async function createJJ(options) {
|
|
|
1875
2015
|
// Get the current operation (the one being undone) to restore its pre-state
|
|
1876
2016
|
const currentOp = ops[ops.length - 1];
|
|
1877
2017
|
|
|
1878
|
-
const previousView = await oplog.undo();
|
|
2018
|
+
const previousView = /** @type {any} */ (await oplog.undo());
|
|
1879
2019
|
|
|
1880
2020
|
// Restore state from previous view
|
|
1881
2021
|
await workingCopy.setCurrentChange(previousView.workingCopy);
|
|
1882
2022
|
|
|
2023
|
+
// Restore ChangeGraph content mutated in place by the operation being
|
|
2024
|
+
// undone (e.g. describe() editing change.description) — see issue #12.
|
|
2025
|
+
// currentOp.changeSnapshot holds the affected change record(s) as they
|
|
2026
|
+
// were BEFORE currentOp ran, captured the same way fileSnapshot is.
|
|
2027
|
+
await graph.load();
|
|
2028
|
+
if (currentOp && currentOp.changeSnapshot) {
|
|
2029
|
+
for (const snapshot of Object.values(
|
|
2030
|
+
/** @type {Record<string, any>} */ (currentOp.changeSnapshot)
|
|
2031
|
+
)) {
|
|
2032
|
+
if (snapshot && (await graph.getChange(snapshot.changeId))) {
|
|
2033
|
+
await graph.updateChange(snapshot);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
|
|
1883
2038
|
// Restore filesystem from previous operation's snapshot (taken BEFORE that operation)
|
|
1884
2039
|
// This is how JJ snapshots the working copy before every command
|
|
1885
2040
|
if (previousView.fileSnapshot) {
|
|
@@ -1913,18 +2068,25 @@ export async function createJJ(options) {
|
|
|
1913
2068
|
if (currentOp && currentOp.conflictsSnapshot) {
|
|
1914
2069
|
// Restore conflicts from the operation we're undoing (its pre-state)
|
|
1915
2070
|
conflicts.conflicts = new Map(Object.entries(currentOp.conflictsSnapshot.conflicts || {}));
|
|
1916
|
-
conflicts.fileConflicts = new Map(
|
|
2071
|
+
conflicts.fileConflicts = new Map(
|
|
2072
|
+
Object.entries(currentOp.conflictsSnapshot.fileConflicts || {})
|
|
2073
|
+
);
|
|
1917
2074
|
await conflicts.save();
|
|
1918
2075
|
} else {
|
|
1919
2076
|
// No conflicts snapshot - clear conflicts
|
|
1920
2077
|
await conflicts.clear();
|
|
1921
2078
|
}
|
|
1922
2079
|
|
|
1923
|
-
// Record undo operation
|
|
2080
|
+
// Record undo operation. Tag it with the view it undid so that redo()
|
|
2081
|
+
// can re-apply it (jj's progressive undo/redo, added in jj v0.33).
|
|
1924
2082
|
await oplog.recordOperation({
|
|
1925
2083
|
timestamp: new Date().toISOString(),
|
|
1926
2084
|
user: await getUserOplogInfo(),
|
|
1927
2085
|
description: 'undo operation',
|
|
2086
|
+
eventType: 'undo',
|
|
2087
|
+
undoneOpId: currentOp.id,
|
|
2088
|
+
undoneView: currentOp.view,
|
|
2089
|
+
undoneConflictsSnapshot: currentOp.conflictsSnapshot || null,
|
|
1928
2090
|
parents: [],
|
|
1929
2091
|
view: previousView,
|
|
1930
2092
|
});
|
|
@@ -1944,13 +2106,110 @@ export async function createJJ(options) {
|
|
|
1944
2106
|
};
|
|
1945
2107
|
},
|
|
1946
2108
|
|
|
2109
|
+
/**
|
|
2110
|
+
* Redo an operation previously undone with undo() (v1.5)
|
|
2111
|
+
*
|
|
2112
|
+
* Mirrors `jj redo` (jj v0.33): progressively re-applies the operations that
|
|
2113
|
+
* `undo()` reverted, most-recent-undo first. Each redo consumes exactly one
|
|
2114
|
+
* pending undo.
|
|
2115
|
+
*
|
|
2116
|
+
* @returns {Promise<Object>} Information about the redone operation
|
|
2117
|
+
*/
|
|
2118
|
+
async redo() {
|
|
2119
|
+
await oplog.load();
|
|
2120
|
+
const ops = await oplog.list();
|
|
2121
|
+
|
|
2122
|
+
// Operations already consumed by a prior redo.
|
|
2123
|
+
const alreadyRedone = new Set();
|
|
2124
|
+
for (const op of ops) {
|
|
2125
|
+
if (op.eventType === 'redo' && op.redoOf) {
|
|
2126
|
+
alreadyRedone.add(op.redoOf);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// Find the most recent undo operation that has not yet been redone.
|
|
2131
|
+
let undoOp = null;
|
|
2132
|
+
for (let i = ops.length - 1; i >= 0; i--) {
|
|
2133
|
+
if (ops[i].eventType === 'undo' && !alreadyRedone.has(ops[i].id)) {
|
|
2134
|
+
undoOp = ops[i];
|
|
2135
|
+
break;
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
|
|
2139
|
+
if (!undoOp) {
|
|
2140
|
+
throw new JJError('NOTHING_TO_REDO', 'No undone operations to redo', {
|
|
2141
|
+
suggestion: 'redo() only reverses a preceding undo()',
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
const redoneView = undoOp.undoneView || {};
|
|
2146
|
+
|
|
2147
|
+
// Restore the working-copy pointer.
|
|
2148
|
+
if (redoneView.workingCopy) {
|
|
2149
|
+
await workingCopy.setCurrentChange(redoneView.workingCopy);
|
|
2150
|
+
}
|
|
2151
|
+
|
|
2152
|
+
// Restore the working-copy files captured in the undone view.
|
|
2153
|
+
if (redoneView.fileSnapshot) {
|
|
2154
|
+
for (const [filePath, content] of Object.entries(redoneView.fileSnapshot)) {
|
|
2155
|
+
try {
|
|
2156
|
+
const fullPath = path.join(dir, filePath);
|
|
2157
|
+
const pathParts = filePath.split('/');
|
|
2158
|
+
if (pathParts.length > 1) {
|
|
2159
|
+
const fullDirPath = path.join(dir, pathParts.slice(0, -1).join('/'));
|
|
2160
|
+
await fs.promises.mkdir(fullDirPath, { recursive: true });
|
|
2161
|
+
}
|
|
2162
|
+
await fs.promises.writeFile(fullPath, content, 'utf8');
|
|
2163
|
+
} catch (error) {
|
|
2164
|
+
throw new JJError(
|
|
2165
|
+
'REDO_FILE_RESTORE_FAILED',
|
|
2166
|
+
`Failed to restore file ${filePath} during redo: ${error.message}`,
|
|
2167
|
+
{ filePath, originalError: error.message }
|
|
2168
|
+
);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
// Restore conflicts to the state that existed after the redone operation.
|
|
2174
|
+
await conflicts.load();
|
|
2175
|
+
if (undoOp.undoneConflictsSnapshot) {
|
|
2176
|
+
conflicts.conflicts = new Map(
|
|
2177
|
+
Object.entries(undoOp.undoneConflictsSnapshot.conflicts || {})
|
|
2178
|
+
);
|
|
2179
|
+
conflicts.fileConflicts = new Map(
|
|
2180
|
+
Object.entries(undoOp.undoneConflictsSnapshot.fileConflicts || {})
|
|
2181
|
+
);
|
|
2182
|
+
await conflicts.save();
|
|
2183
|
+
}
|
|
2184
|
+
|
|
2185
|
+
await oplog.recordOperation({
|
|
2186
|
+
timestamp: new Date().toISOString(),
|
|
2187
|
+
user: await getUserOplogInfo(),
|
|
2188
|
+
description: 'redo operation',
|
|
2189
|
+
eventType: 'redo',
|
|
2190
|
+
redoOf: undoOp.id,
|
|
2191
|
+
parents: [],
|
|
2192
|
+
view: redoneView,
|
|
2193
|
+
});
|
|
2194
|
+
|
|
2195
|
+
return {
|
|
2196
|
+
redoneOperation: {
|
|
2197
|
+
description: undoOp.description,
|
|
2198
|
+
undoneOpId: undoOp.undoneOpId,
|
|
2199
|
+
},
|
|
2200
|
+
restoredState: {
|
|
2201
|
+
workingCopy: redoneView.workingCopy,
|
|
2202
|
+
heads: redoneView.heads,
|
|
2203
|
+
fileCount: redoneView.fileSnapshot ? Object.keys(redoneView.fileSnapshot).length : 0,
|
|
2204
|
+
},
|
|
2205
|
+
};
|
|
2206
|
+
},
|
|
2207
|
+
|
|
1947
2208
|
/**
|
|
1948
2209
|
* View operation log (operation history)
|
|
1949
2210
|
*
|
|
1950
|
-
* @param {
|
|
1951
|
-
* @
|
|
1952
|
-
* @param {string} [opts.change] - Show operations for specific change
|
|
1953
|
-
* @returns {Promise<Array>} Array of operations
|
|
2211
|
+
* @param {Record<string, any>} [opts] - Options
|
|
2212
|
+
* @returns {Promise<Array<any>>} Array of operations
|
|
1954
2213
|
*/
|
|
1955
2214
|
async obslog(opts = {}) {
|
|
1956
2215
|
await oplog.load();
|
|
@@ -1961,11 +2220,11 @@ export async function createJJ(options) {
|
|
|
1961
2220
|
|
|
1962
2221
|
// Filter by change if specified
|
|
1963
2222
|
if (changeId) {
|
|
1964
|
-
operations = operations.filter(
|
|
1965
|
-
op
|
|
1966
|
-
op.view
|
|
1967
|
-
(op.view.
|
|
1968
|
-
|
|
2223
|
+
operations = operations.filter(
|
|
2224
|
+
(op) =>
|
|
2225
|
+
op.view &&
|
|
2226
|
+
(op.view.workingCopy === changeId ||
|
|
2227
|
+
(op.view.heads && op.view.heads.includes(changeId)))
|
|
1969
2228
|
);
|
|
1970
2229
|
}
|
|
1971
2230
|
|
|
@@ -1975,10 +2234,10 @@ export async function createJJ(options) {
|
|
|
1975
2234
|
}
|
|
1976
2235
|
|
|
1977
2236
|
// Transform to evolution events with operation field
|
|
1978
|
-
const events = operations.map(op => ({
|
|
2237
|
+
const events = operations.map((op) => ({
|
|
1979
2238
|
eventType: op.eventType || 'modify',
|
|
1980
2239
|
description: op.description,
|
|
1981
|
-
operation: op.id,
|
|
2240
|
+
operation: op.id, // Add operation ID
|
|
1982
2241
|
timestamp: op.timestamp,
|
|
1983
2242
|
user: op.user,
|
|
1984
2243
|
}));
|
|
@@ -1994,9 +2253,8 @@ export async function createJJ(options) {
|
|
|
1994
2253
|
/**
|
|
1995
2254
|
* List all operations
|
|
1996
2255
|
*
|
|
1997
|
-
* @param {
|
|
1998
|
-
* @
|
|
1999
|
-
* @returns {Promise<Array>} Array of operations
|
|
2256
|
+
* @param {Record<string, any>} [opts] - Options
|
|
2257
|
+
* @returns {Promise<Array<any>>} Array of operations
|
|
2000
2258
|
*/
|
|
2001
2259
|
async list(opts = {}) {
|
|
2002
2260
|
await oplog.load();
|
|
@@ -2012,20 +2270,19 @@ export async function createJJ(options) {
|
|
|
2012
2270
|
/**
|
|
2013
2271
|
* View repository at a specific operation (time travel)
|
|
2014
2272
|
*
|
|
2015
|
-
* @param {
|
|
2016
|
-
* @param {string} args.operation - Operation ID to view at
|
|
2273
|
+
* @param {Record<string, any>} args - Arguments
|
|
2017
2274
|
* @returns {Promise<Object>} Read-only repository view at that operation
|
|
2018
2275
|
*/
|
|
2019
2276
|
async at(args) {
|
|
2020
2277
|
if (!args || !args.operation) {
|
|
2021
2278
|
throw new JJError('INVALID_ARGUMENT', 'Missing operation ID', {
|
|
2022
|
-
suggestion: 'Provide { operation: operationId }'
|
|
2279
|
+
suggestion: 'Provide { operation: operationId }',
|
|
2023
2280
|
});
|
|
2024
2281
|
}
|
|
2025
2282
|
|
|
2026
2283
|
await oplog.load();
|
|
2027
2284
|
const ops = await oplog.list();
|
|
2028
|
-
const targetOp = ops.find(op => op.id === args.operation);
|
|
2285
|
+
const targetOp = ops.find((op) => op.id === args.operation);
|
|
2029
2286
|
|
|
2030
2287
|
if (!targetOp) {
|
|
2031
2288
|
throw new JJError('OPERATION_NOT_FOUND', `Operation ${args.operation} not found`);
|
|
@@ -2033,17 +2290,17 @@ export async function createJJ(options) {
|
|
|
2033
2290
|
|
|
2034
2291
|
// Create a read-only view at this operation
|
|
2035
2292
|
return {
|
|
2036
|
-
async log(
|
|
2293
|
+
async log(_logArgs = {}) {
|
|
2037
2294
|
// Get changes that existed at this operation
|
|
2038
2295
|
await graph.load();
|
|
2039
2296
|
const allChanges = await graph.getAllChanges();
|
|
2040
2297
|
|
|
2041
2298
|
// Filter to changes that existed before or at this operation
|
|
2042
2299
|
const opIndex = ops.indexOf(targetOp);
|
|
2043
|
-
const validChanges = allChanges.filter(change => {
|
|
2300
|
+
const validChanges = allChanges.filter((/** @type {any} */ change) => {
|
|
2044
2301
|
// Check if this change was created before this operation
|
|
2045
|
-
const changeOps = ops.filter(
|
|
2046
|
-
op.view && op.view.heads && op.view.heads.includes(change.changeId)
|
|
2302
|
+
const changeOps = ops.filter(
|
|
2303
|
+
(op) => op.view && op.view.heads && op.view.heads.includes(change.changeId)
|
|
2047
2304
|
);
|
|
2048
2305
|
return changeOps.length > 0 && ops.indexOf(changeOps[0]) <= opIndex;
|
|
2049
2306
|
});
|
|
@@ -2067,8 +2324,7 @@ export async function createJJ(options) {
|
|
|
2067
2324
|
/**
|
|
2068
2325
|
* Show changes in a specific operation (matches `jj operation show`)
|
|
2069
2326
|
*
|
|
2070
|
-
* @param {
|
|
2071
|
-
* @param {string} args.operation - Operation ID
|
|
2327
|
+
* @param {Record<string, any>} args - Arguments
|
|
2072
2328
|
* @returns {Promise<Object>} Operation details with changes made
|
|
2073
2329
|
*/
|
|
2074
2330
|
async show(args) {
|
|
@@ -2080,7 +2336,7 @@ export async function createJJ(options) {
|
|
|
2080
2336
|
|
|
2081
2337
|
await oplog.load();
|
|
2082
2338
|
const ops = await oplog.list();
|
|
2083
|
-
const op = ops.find(o => o.id === args.operation);
|
|
2339
|
+
const op = ops.find((o) => o.id === args.operation);
|
|
2084
2340
|
|
|
2085
2341
|
if (!op) {
|
|
2086
2342
|
throw new JJError('OPERATION_NOT_FOUND', `Operation ${args.operation} not found`);
|
|
@@ -2112,9 +2368,7 @@ export async function createJJ(options) {
|
|
|
2112
2368
|
/**
|
|
2113
2369
|
* Compare repository state between two operations (matches `jj operation diff`)
|
|
2114
2370
|
*
|
|
2115
|
-
* @param {
|
|
2116
|
-
* @param {string} args.from - Source operation ID
|
|
2117
|
-
* @param {string} args.to - Target operation ID
|
|
2371
|
+
* @param {Record<string, any>} args - Arguments
|
|
2118
2372
|
* @returns {Promise<Object>} Differences between operations
|
|
2119
2373
|
*/
|
|
2120
2374
|
async diff(args) {
|
|
@@ -2126,8 +2380,8 @@ export async function createJJ(options) {
|
|
|
2126
2380
|
|
|
2127
2381
|
await oplog.load();
|
|
2128
2382
|
const ops = await oplog.list();
|
|
2129
|
-
const fromOp = ops.find(o => o.id === args.from);
|
|
2130
|
-
const toOp = ops.find(o => o.id === args.to);
|
|
2383
|
+
const fromOp = ops.find((o) => o.id === args.from);
|
|
2384
|
+
const toOp = ops.find((o) => o.id === args.to);
|
|
2131
2385
|
|
|
2132
2386
|
if (!fromOp) {
|
|
2133
2387
|
throw new JJError('OPERATION_NOT_FOUND', `Operation ${args.from} not found`);
|
|
@@ -2140,13 +2394,14 @@ export async function createJJ(options) {
|
|
|
2140
2394
|
const fromHeads = new Set(fromOp.view?.heads || []);
|
|
2141
2395
|
const toHeads = new Set(toOp.view?.heads || []);
|
|
2142
2396
|
|
|
2143
|
-
const addedHeads = [...toHeads].filter(h => !fromHeads.has(h));
|
|
2144
|
-
const removedHeads = [...fromHeads].filter(h => !toHeads.has(h));
|
|
2397
|
+
const addedHeads = [...toHeads].filter((h) => !fromHeads.has(h));
|
|
2398
|
+
const removedHeads = [...fromHeads].filter((h) => !toHeads.has(h));
|
|
2145
2399
|
|
|
2146
2400
|
// Compare bookmarks
|
|
2147
2401
|
const fromBookmarks = fromOp.view?.bookmarks || {};
|
|
2148
2402
|
const toBookmarks = toOp.view?.bookmarks || {};
|
|
2149
2403
|
|
|
2404
|
+
/** @type {Record<string, any>} */
|
|
2150
2405
|
const bookmarkChanges = {};
|
|
2151
2406
|
for (const name of new Set([...Object.keys(fromBookmarks), ...Object.keys(toBookmarks)])) {
|
|
2152
2407
|
if (fromBookmarks[name] !== toBookmarks[name]) {
|
|
@@ -2172,8 +2427,7 @@ export async function createJJ(options) {
|
|
|
2172
2427
|
/**
|
|
2173
2428
|
* Restore repository to a specific operation (matches `jj operation restore`)
|
|
2174
2429
|
*
|
|
2175
|
-
* @param {
|
|
2176
|
-
* @param {string} args.operation - Operation ID to restore to
|
|
2430
|
+
* @param {Record<string, any>} args - Arguments
|
|
2177
2431
|
* @returns {Promise<Object>} Restore result
|
|
2178
2432
|
*/
|
|
2179
2433
|
async restore(args) {
|
|
@@ -2185,7 +2439,7 @@ export async function createJJ(options) {
|
|
|
2185
2439
|
|
|
2186
2440
|
await oplog.load();
|
|
2187
2441
|
const ops = await oplog.list();
|
|
2188
|
-
const targetOp = ops.find(o => o.id === args.operation);
|
|
2442
|
+
const targetOp = ops.find((o) => o.id === args.operation);
|
|
2189
2443
|
|
|
2190
2444
|
if (!targetOp) {
|
|
2191
2445
|
throw new JJError('OPERATION_NOT_FOUND', `Operation ${args.operation} not found`);
|
|
@@ -2234,8 +2488,7 @@ export async function createJJ(options) {
|
|
|
2234
2488
|
* This creates a new operation that undoes the effects of the specified operation,
|
|
2235
2489
|
* without affecting any operations that came after it.
|
|
2236
2490
|
*
|
|
2237
|
-
* @param {
|
|
2238
|
-
* @param {string} args.operation - Operation ID to revert
|
|
2491
|
+
* @param {Record<string, any>} args - Arguments
|
|
2239
2492
|
* @returns {Promise<Object>} Revert result
|
|
2240
2493
|
*/
|
|
2241
2494
|
async revert(args) {
|
|
@@ -2247,7 +2500,7 @@ export async function createJJ(options) {
|
|
|
2247
2500
|
|
|
2248
2501
|
await oplog.load();
|
|
2249
2502
|
const ops = await oplog.list();
|
|
2250
|
-
const targetOp = ops.find(o => o.id === args.operation);
|
|
2503
|
+
const targetOp = ops.find((o) => o.id === args.operation);
|
|
2251
2504
|
|
|
2252
2505
|
if (!targetOp) {
|
|
2253
2506
|
throw new JJError('OPERATION_NOT_FOUND', `Operation ${args.operation} not found`);
|
|
@@ -2264,10 +2517,11 @@ export async function createJJ(options) {
|
|
|
2264
2517
|
const previousOp = ops[targetIndex - 1];
|
|
2265
2518
|
|
|
2266
2519
|
// Compute the inverse changes
|
|
2267
|
-
const inversChanges = {
|
|
2520
|
+
const inversChanges = /** @type {Record<string, any>} */ ({
|
|
2268
2521
|
bookmarks: {},
|
|
2522
|
+
tags: {},
|
|
2269
2523
|
heads: [],
|
|
2270
|
-
};
|
|
2524
|
+
});
|
|
2271
2525
|
|
|
2272
2526
|
// Revert bookmark changes
|
|
2273
2527
|
const prevBookmarks = previousOp.view?.bookmarks || {};
|
|
@@ -2280,8 +2534,9 @@ export async function createJJ(options) {
|
|
|
2280
2534
|
await bookmarks.delete(name);
|
|
2281
2535
|
inversChanges.bookmarks[name] = { action: 'deleted' };
|
|
2282
2536
|
} else if (prevBookmarks[name] !== targetBookmarks[name]) {
|
|
2283
|
-
// Bookmark was moved - move it back
|
|
2284
|
-
|
|
2537
|
+
// Bookmark was moved - move it back (the bookmark still exists, so
|
|
2538
|
+
// use move() rather than set(), which is create-only).
|
|
2539
|
+
await bookmarks.move(name, prevBookmarks[name]);
|
|
2285
2540
|
inversChanges.bookmarks[name] = {
|
|
2286
2541
|
action: 'moved',
|
|
2287
2542
|
from: targetBookmarks[name],
|
|
@@ -2304,6 +2559,46 @@ export async function createJJ(options) {
|
|
|
2304
2559
|
|
|
2305
2560
|
await bookmarks.save();
|
|
2306
2561
|
|
|
2562
|
+
// Revert tag changes (mirrors the bookmark diff above — issue #12).
|
|
2563
|
+
// Unlike bookmarks, TagStore has no "move" primitive (tags are meant
|
|
2564
|
+
// to be immutable once created), so a moved tag is reverted via
|
|
2565
|
+
// delete+recreate, matching how tag.set() itself updates a tag.
|
|
2566
|
+
const prevTags = previousOp.view?.tags || {};
|
|
2567
|
+
const targetTags = targetOp.view?.tags || {};
|
|
2568
|
+
|
|
2569
|
+
for (const name of Object.keys(targetTags)) {
|
|
2570
|
+
if (!prevTags[name]) {
|
|
2571
|
+
// Tag was added - delete it
|
|
2572
|
+
if (await tags.exists(name)) {
|
|
2573
|
+
await tags.delete(name);
|
|
2574
|
+
}
|
|
2575
|
+
inversChanges.tags[name] = { action: 'deleted' };
|
|
2576
|
+
} else if (prevTags[name] !== targetTags[name]) {
|
|
2577
|
+
// Tag was moved - restore old target
|
|
2578
|
+
if (await tags.exists(name)) {
|
|
2579
|
+
await tags.delete(name);
|
|
2580
|
+
}
|
|
2581
|
+
await tags.create(name, prevTags[name]);
|
|
2582
|
+
inversChanges.tags[name] = {
|
|
2583
|
+
action: 'moved',
|
|
2584
|
+
from: targetTags[name],
|
|
2585
|
+
to: prevTags[name],
|
|
2586
|
+
};
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
// Check for deleted tags (existed before, don't exist after)
|
|
2591
|
+
for (const name of Object.keys(prevTags)) {
|
|
2592
|
+
if (!targetTags[name] && !(await tags.exists(name))) {
|
|
2593
|
+
// Tag was deleted - restore it
|
|
2594
|
+
await tags.create(name, prevTags[name]);
|
|
2595
|
+
inversChanges.tags[name] = {
|
|
2596
|
+
action: 'restored',
|
|
2597
|
+
to: prevTags[name],
|
|
2598
|
+
};
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
|
|
2307
2602
|
// Revert working copy change if it changed
|
|
2308
2603
|
if (previousOp.view?.workingCopy !== targetOp.view?.workingCopy) {
|
|
2309
2604
|
await workingCopy.load();
|
|
@@ -2318,6 +2613,7 @@ export async function createJJ(options) {
|
|
|
2318
2613
|
parents: [],
|
|
2319
2614
|
view: {
|
|
2320
2615
|
bookmarks: prevBookmarks,
|
|
2616
|
+
tags: prevTags,
|
|
2321
2617
|
remoteBookmarks: previousOp.view?.remoteBookmarks || {},
|
|
2322
2618
|
heads: previousOp.view?.heads || [],
|
|
2323
2619
|
workingCopy: previousOp.view?.workingCopy,
|
|
@@ -2341,8 +2637,7 @@ export async function createJJ(options) {
|
|
|
2341
2637
|
* WARNING: This is a destructive operation that cannot be undone. Use with caution.
|
|
2342
2638
|
* In most cases, you should use undo(), restore(), or revert() instead.
|
|
2343
2639
|
*
|
|
2344
|
-
* @param {
|
|
2345
|
-
* @param {string} args.operation - Operation ID to abandon
|
|
2640
|
+
* @param {Record<string, any>} args - Arguments
|
|
2346
2641
|
* @returns {Promise<Object>} Abandon result with relinked children
|
|
2347
2642
|
*/
|
|
2348
2643
|
async abandon(args) {
|
|
@@ -2355,7 +2650,7 @@ export async function createJJ(options) {
|
|
|
2355
2650
|
await oplog.load();
|
|
2356
2651
|
|
|
2357
2652
|
// Call the OperationLog abandon method
|
|
2358
|
-
const result = await oplog.abandon(args.operation);
|
|
2653
|
+
const result = /** @type {any} */ (await oplog.abandon(args.operation));
|
|
2359
2654
|
|
|
2360
2655
|
// Record a new operation about the abandonment
|
|
2361
2656
|
// Note: This creates a meta-record that an operation was abandoned
|
|
@@ -2372,7 +2667,7 @@ export async function createJJ(options) {
|
|
|
2372
2667
|
},
|
|
2373
2668
|
metadata: {
|
|
2374
2669
|
abandonedOperation: args.operation,
|
|
2375
|
-
relinkedChildren: result.relinkedChildren.map(c => c.operationId),
|
|
2670
|
+
relinkedChildren: result.relinkedChildren.map((/** @type {any} */ c) => c.operationId),
|
|
2376
2671
|
},
|
|
2377
2672
|
});
|
|
2378
2673
|
|
|
@@ -2388,14 +2683,11 @@ export async function createJJ(options) {
|
|
|
2388
2683
|
// ========================================
|
|
2389
2684
|
// v0.2 FEATURES: History Editing
|
|
2390
2685
|
// ========================================
|
|
2391
|
-
|
|
2686
|
+
|
|
2392
2687
|
/**
|
|
2393
2688
|
* Squash source change into destination change
|
|
2394
2689
|
*
|
|
2395
|
-
* @param {
|
|
2396
|
-
* @param {string} [args.source] - Source change ID to squash (defaults to @ - working copy)
|
|
2397
|
-
* @param {string} [args.dest] - Destination change ID (if source is @, defaults to parent)
|
|
2398
|
-
* @param {string} [args.into] - Alias for dest (matches JJ CLI)
|
|
2690
|
+
* @param {Record<string, any>} args - Arguments
|
|
2399
2691
|
*/
|
|
2400
2692
|
async squash(args = {}) {
|
|
2401
2693
|
// Check for interactive mode (not supported)
|
|
@@ -2426,7 +2718,11 @@ export async function createJJ(options) {
|
|
|
2426
2718
|
let destChangeId = dest;
|
|
2427
2719
|
if (!destChangeId && source === workingCopy.getCurrentChangeId()) {
|
|
2428
2720
|
const workingCopyChange = await graph.getChange(source);
|
|
2429
|
-
if (
|
|
2721
|
+
if (
|
|
2722
|
+
workingCopyChange &&
|
|
2723
|
+
workingCopyChange.parents &&
|
|
2724
|
+
workingCopyChange.parents.length > 0
|
|
2725
|
+
) {
|
|
2430
2726
|
destChangeId = workingCopyChange.parents[0];
|
|
2431
2727
|
} else {
|
|
2432
2728
|
throw new JJError('INVALID_ARGUMENTS', 'Cannot squash working copy: no parent found');
|
|
@@ -2473,7 +2769,7 @@ export async function createJJ(options) {
|
|
|
2473
2769
|
|
|
2474
2770
|
// If squashing the working copy, create a new empty working copy on top of dest
|
|
2475
2771
|
if (isSquashingWorkingCopy) {
|
|
2476
|
-
const user = userConfig.getUser();
|
|
2772
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
2477
2773
|
const newChangeId = generateChangeId();
|
|
2478
2774
|
const newChange = {
|
|
2479
2775
|
changeId: newChangeId,
|
|
@@ -2495,7 +2791,7 @@ export async function createJJ(options) {
|
|
|
2495
2791
|
fileSnapshot: {},
|
|
2496
2792
|
};
|
|
2497
2793
|
|
|
2498
|
-
await graph.addChange(newChange);
|
|
2794
|
+
await graph.addChange(newChange); // Middleware will sync to Git
|
|
2499
2795
|
await workingCopy.setCurrentChange(newChangeId);
|
|
2500
2796
|
newWorkingCopyId = newChangeId;
|
|
2501
2797
|
}
|
|
@@ -2515,27 +2811,29 @@ export async function createJJ(options) {
|
|
|
2515
2811
|
});
|
|
2516
2812
|
|
|
2517
2813
|
// Dispatch change:squashed event (informational)
|
|
2518
|
-
await dispatchEventAsync(
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2814
|
+
await dispatchEventAsync(
|
|
2815
|
+
jj,
|
|
2816
|
+
'change:squashed',
|
|
2817
|
+
{
|
|
2818
|
+
operation: 'squash',
|
|
2819
|
+
sourceChangeId: source,
|
|
2820
|
+
destChangeId: destChangeId,
|
|
2821
|
+
sourceChange,
|
|
2822
|
+
destChange,
|
|
2823
|
+
timestamp: new Date().toISOString(),
|
|
2824
|
+
},
|
|
2825
|
+
{ cancelable: false }
|
|
2826
|
+
);
|
|
2526
2827
|
|
|
2527
2828
|
return destChange;
|
|
2528
2829
|
},
|
|
2529
|
-
|
|
2830
|
+
|
|
2530
2831
|
/**
|
|
2531
2832
|
* Rebase a change to a new parent (matches `jj rebase`)
|
|
2532
2833
|
*
|
|
2533
2834
|
* This is the proper JJ CLI semantic for moving changes in history.
|
|
2534
2835
|
*
|
|
2535
|
-
* @param {
|
|
2536
|
-
* @param {string} args.changeId - Change ID to rebase (or use `from` for compatibility)
|
|
2537
|
-
* @param {string} args.newParent - New parent change ID (or use `to` for compatibility)
|
|
2538
|
-
* @param {Array<string>} [args.paths] - Optional: only rebase changes to specific paths
|
|
2836
|
+
* @param {Record<string, any>} args - Arguments
|
|
2539
2837
|
* @returns {Promise<Object>} Updated change object
|
|
2540
2838
|
*
|
|
2541
2839
|
* @example
|
|
@@ -2556,8 +2854,7 @@ export async function createJJ(options) {
|
|
|
2556
2854
|
/**
|
|
2557
2855
|
* Abandon a change
|
|
2558
2856
|
*
|
|
2559
|
-
* @param {
|
|
2560
|
-
* @param {string} [args.changeId] - Change ID to abandon (defaults to @ - working copy)
|
|
2857
|
+
* @param {Record<string, any>} [args] - Arguments
|
|
2561
2858
|
* @returns {Promise<Object>} The abandoned change object including changeId, description, and abandoned flag
|
|
2562
2859
|
*/
|
|
2563
2860
|
async abandon(args = {}) {
|
|
@@ -2569,7 +2866,7 @@ export async function createJJ(options) {
|
|
|
2569
2866
|
const changeId = args.changeId || workingCopy.getCurrentChangeId();
|
|
2570
2867
|
|
|
2571
2868
|
const change = await graph.getChange(changeId);
|
|
2572
|
-
const user = userConfig.getUser();
|
|
2869
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
2573
2870
|
|
|
2574
2871
|
if (!change) {
|
|
2575
2872
|
throw new JJError('CHANGE_NOT_FOUND', `Change ${changeId} not found`);
|
|
@@ -2601,12 +2898,17 @@ export async function createJJ(options) {
|
|
|
2601
2898
|
});
|
|
2602
2899
|
|
|
2603
2900
|
// Dispatch change:abandoned event (informational)
|
|
2604
|
-
await dispatchEventAsync(
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2901
|
+
await dispatchEventAsync(
|
|
2902
|
+
jj,
|
|
2903
|
+
'change:abandoned',
|
|
2904
|
+
{
|
|
2905
|
+
operation: 'abandon',
|
|
2906
|
+
changeId: changeId,
|
|
2907
|
+
change,
|
|
2908
|
+
timestamp: new Date().toISOString(),
|
|
2909
|
+
},
|
|
2910
|
+
{ cancelable: false }
|
|
2911
|
+
);
|
|
2610
2912
|
|
|
2611
2913
|
return change;
|
|
2612
2914
|
},
|
|
@@ -2617,9 +2919,7 @@ export async function createJJ(options) {
|
|
|
2617
2919
|
* Similar to `git revert`, this creates a new change that undoes the changes made
|
|
2618
2920
|
* by the specified revision without modifying the original change.
|
|
2619
2921
|
*
|
|
2620
|
-
* @param {
|
|
2621
|
-
* @param {string} args.revision - Change ID to back out
|
|
2622
|
-
* @param {string} [args.message] - Custom description for the backout change
|
|
2922
|
+
* @param {Record<string, any>} args - Arguments
|
|
2623
2923
|
* @returns {Promise<Object>} The new backout change including changeId, description, and backedOut field
|
|
2624
2924
|
*/
|
|
2625
2925
|
async backout(args) {
|
|
@@ -2634,7 +2934,8 @@ export async function createJJ(options) {
|
|
|
2634
2934
|
|
|
2635
2935
|
if (!args || !args.revision) {
|
|
2636
2936
|
throw new JJError('INVALID_ARGUMENT', 'Missing revision argument', {
|
|
2637
|
-
suggestion:
|
|
2937
|
+
suggestion:
|
|
2938
|
+
'Provide the change ID to back out: { revision: "abc123..." } (or use change/changeId/target)',
|
|
2638
2939
|
});
|
|
2639
2940
|
}
|
|
2640
2941
|
|
|
@@ -2643,7 +2944,7 @@ export async function createJJ(options) {
|
|
|
2643
2944
|
await userConfig.load();
|
|
2644
2945
|
|
|
2645
2946
|
const originalChange = await graph.getChange(args.revision);
|
|
2646
|
-
const user = userConfig.getUser();
|
|
2947
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
2647
2948
|
|
|
2648
2949
|
if (!originalChange) {
|
|
2649
2950
|
throw new JJError('CHANGE_NOT_FOUND', `Change ${args.revision} not found`);
|
|
@@ -2713,6 +3014,112 @@ export async function createJJ(options) {
|
|
|
2713
3014
|
};
|
|
2714
3015
|
},
|
|
2715
3016
|
|
|
3017
|
+
/**
|
|
3018
|
+
* Revert a change by creating a new change that undoes it (v1.5)
|
|
3019
|
+
*
|
|
3020
|
+
* This is the canonical name in modern Jujutsu (`jj revert`), which fully
|
|
3021
|
+
* replaced `jj backout` in jj v0.35. `backout()` remains as a deprecated
|
|
3022
|
+
* alias for backward compatibility.
|
|
3023
|
+
*
|
|
3024
|
+
* @param {Record<string, any>} args - Revert arguments
|
|
3025
|
+
* @returns {Promise<Object>} Result including `revertedFrom`
|
|
3026
|
+
*/
|
|
3027
|
+
async revert(args) {
|
|
3028
|
+
const result = await jj.backout(args);
|
|
3029
|
+
return {
|
|
3030
|
+
changeId: result.changeId,
|
|
3031
|
+
description: result.description,
|
|
3032
|
+
revertedFrom: result.backedOut,
|
|
3033
|
+
// Keep the legacy field so existing callers keep working.
|
|
3034
|
+
backedOut: result.backedOut,
|
|
3035
|
+
fileSnapshot: result.fileSnapshot,
|
|
3036
|
+
};
|
|
3037
|
+
},
|
|
3038
|
+
|
|
3039
|
+
/**
|
|
3040
|
+
* Sign a change (matches `jj sign`, jj v0.27)
|
|
3041
|
+
*
|
|
3042
|
+
* A pure-JS library cannot verify GPG/SSH keys the way the jj CLI does, so
|
|
3043
|
+
* this records signature *metadata* on the change (backend/key/status). This
|
|
3044
|
+
* makes changes discoverable via the `signed()` revset and lets tooling
|
|
3045
|
+
* round-trip signature intent. Pass a real signer via `args.backend`/`args.key`.
|
|
3046
|
+
*
|
|
3047
|
+
* @param {Record<string, any>} [args] - Arguments
|
|
3048
|
+
* @returns {Promise<Object>} { changeId, signed, signature }
|
|
3049
|
+
*/
|
|
3050
|
+
async sign(args = {}) {
|
|
3051
|
+
await graph.load();
|
|
3052
|
+
await workingCopy.load();
|
|
3053
|
+
|
|
3054
|
+
const revision =
|
|
3055
|
+
args.revision || args.change || args.changeId || workingCopy.getCurrentChangeId();
|
|
3056
|
+
const change = await graph.getChange(revision);
|
|
3057
|
+
if (!change) {
|
|
3058
|
+
throw new JJError('CHANGE_NOT_FOUND', `Change ${revision} not found`);
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
const signature = {
|
|
3062
|
+
status: 'good',
|
|
3063
|
+
backend: args.backend || 'none',
|
|
3064
|
+
key: args.key || null,
|
|
3065
|
+
timestamp: new Date().toISOString(),
|
|
3066
|
+
};
|
|
3067
|
+
|
|
3068
|
+
await graph.updateChange({ ...change, signed: true, signature });
|
|
3069
|
+
|
|
3070
|
+
await oplog.recordOperation({
|
|
3071
|
+
timestamp: new Date().toISOString(),
|
|
3072
|
+
user: await getUserOplogInfo(),
|
|
3073
|
+
description: `sign ${revision.slice(0, 8)}`,
|
|
3074
|
+
parents: [],
|
|
3075
|
+
view: {
|
|
3076
|
+
bookmarks: {},
|
|
3077
|
+
remoteBookmarks: {},
|
|
3078
|
+
heads: [revision],
|
|
3079
|
+
workingCopy: workingCopy.getCurrentChangeId(),
|
|
3080
|
+
},
|
|
3081
|
+
});
|
|
3082
|
+
|
|
3083
|
+
return { changeId: revision, signed: true, signature };
|
|
3084
|
+
},
|
|
3085
|
+
|
|
3086
|
+
/**
|
|
3087
|
+
* Remove a signature from a change (matches `jj unsign`, jj v0.27)
|
|
3088
|
+
*
|
|
3089
|
+
* @param {Record<string, any>} [args] - Arguments
|
|
3090
|
+
* @returns {Promise<Object>} { changeId, signed }
|
|
3091
|
+
*/
|
|
3092
|
+
async unsign(args = {}) {
|
|
3093
|
+
await graph.load();
|
|
3094
|
+
await workingCopy.load();
|
|
3095
|
+
|
|
3096
|
+
const revision =
|
|
3097
|
+
args.revision || args.change || args.changeId || workingCopy.getCurrentChangeId();
|
|
3098
|
+
const change = await graph.getChange(revision);
|
|
3099
|
+
if (!change) {
|
|
3100
|
+
throw new JJError('CHANGE_NOT_FOUND', `Change ${revision} not found`);
|
|
3101
|
+
}
|
|
3102
|
+
|
|
3103
|
+
const updated = { ...change, signed: false };
|
|
3104
|
+
delete updated.signature;
|
|
3105
|
+
await graph.updateChange(updated);
|
|
3106
|
+
|
|
3107
|
+
await oplog.recordOperation({
|
|
3108
|
+
timestamp: new Date().toISOString(),
|
|
3109
|
+
user: await getUserOplogInfo(),
|
|
3110
|
+
description: `unsign ${revision.slice(0, 8)}`,
|
|
3111
|
+
parents: [],
|
|
3112
|
+
view: {
|
|
3113
|
+
bookmarks: {},
|
|
3114
|
+
remoteBookmarks: {},
|
|
3115
|
+
heads: [revision],
|
|
3116
|
+
workingCopy: workingCopy.getCurrentChangeId(),
|
|
3117
|
+
},
|
|
3118
|
+
});
|
|
3119
|
+
|
|
3120
|
+
return { changeId: revision, signed: false };
|
|
3121
|
+
},
|
|
3122
|
+
|
|
2716
3123
|
/**
|
|
2717
3124
|
* Absorb working copy changes into ancestor changes
|
|
2718
3125
|
*
|
|
@@ -2727,8 +3134,7 @@ export async function createJJ(options) {
|
|
|
2727
3134
|
* 4. Update each ancestor with its modifications
|
|
2728
3135
|
* 5. Remove absorbed changes from working copy
|
|
2729
3136
|
*
|
|
2730
|
-
* @param {
|
|
2731
|
-
* @param {boolean} [args.dryRun] - Preview without making changes
|
|
3137
|
+
* @param {Record<string, any>} [args] - Optional arguments
|
|
2732
3138
|
* @returns {Promise<Object>} Result with affected changes
|
|
2733
3139
|
*/
|
|
2734
3140
|
async absorb(args = {}) {
|
|
@@ -2738,7 +3144,7 @@ export async function createJJ(options) {
|
|
|
2738
3144
|
|
|
2739
3145
|
const workingChangeId = workingCopy.getCurrentChangeId();
|
|
2740
3146
|
const workingChange = await graph.getChange(workingChangeId);
|
|
2741
|
-
const user = userConfig.getUser();
|
|
3147
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
2742
3148
|
|
|
2743
3149
|
if (!workingChange) {
|
|
2744
3150
|
// No working copy
|
|
@@ -2767,6 +3173,7 @@ export async function createJJ(options) {
|
|
|
2767
3173
|
await graph.updateChange(workingChange);
|
|
2768
3174
|
|
|
2769
3175
|
// Get parent snapshot to determine what's modified
|
|
3176
|
+
/** @type {Record<string, any>} */
|
|
2770
3177
|
let parentSnapshot = {};
|
|
2771
3178
|
if (workingChange.parents && workingChange.parents.length > 0) {
|
|
2772
3179
|
const parent = await graph.getChange(workingChange.parents[0]);
|
|
@@ -2776,6 +3183,7 @@ export async function createJJ(options) {
|
|
|
2776
3183
|
}
|
|
2777
3184
|
|
|
2778
3185
|
// Find modified files (existed in parent AND content is different)
|
|
3186
|
+
/** @type {Record<string, any>} */
|
|
2779
3187
|
const modifiedFiles = {};
|
|
2780
3188
|
for (const [filePath, content] of Object.entries(currentSnapshot)) {
|
|
2781
3189
|
// Only count as modified if file existed in parent (not a new file)
|
|
@@ -2792,6 +3200,7 @@ export async function createJJ(options) {
|
|
|
2792
3200
|
}
|
|
2793
3201
|
|
|
2794
3202
|
// Check if there are new files (not in parent)
|
|
3203
|
+
/** @type {Record<string, any>} */
|
|
2795
3204
|
const newFiles = {};
|
|
2796
3205
|
for (const [filePath, content] of Object.entries(currentSnapshot)) {
|
|
2797
3206
|
if (parentSnapshot[filePath] === undefined) {
|
|
@@ -2816,6 +3225,7 @@ export async function createJJ(options) {
|
|
|
2816
3225
|
|
|
2817
3226
|
// For each modified file, use line-level tracking to find which ancestor
|
|
2818
3227
|
// modified each line, then group modifications by ancestor
|
|
3228
|
+
/** @type {Record<string, any>} */
|
|
2819
3229
|
const ancestorModifications = {}; // ancestorId -> { filePath: content }
|
|
2820
3230
|
|
|
2821
3231
|
for (const filePath of Object.keys(modifiedFiles)) {
|
|
@@ -2943,9 +3353,8 @@ export async function createJJ(options) {
|
|
|
2943
3353
|
// Update working copy to reflect absorbed changes
|
|
2944
3354
|
// Working copy should now match parent's new state (after absorption) plus any new files
|
|
2945
3355
|
const updatedParent = await graph.getChange(workingChange.parents[0]);
|
|
2946
|
-
const newWorkingSnapshot =
|
|
2947
|
-
? { ...updatedParent.fileSnapshot }
|
|
2948
|
-
: {};
|
|
3356
|
+
const newWorkingSnapshot =
|
|
3357
|
+
updatedParent && updatedParent.fileSnapshot ? { ...updatedParent.fileSnapshot } : {};
|
|
2949
3358
|
|
|
2950
3359
|
// Add new files that weren't absorbed
|
|
2951
3360
|
for (const [filePath, content] of Object.entries(newFiles)) {
|
|
@@ -3049,7 +3458,7 @@ export async function createJJ(options) {
|
|
|
3049
3458
|
* @param {string} parentContent - Content from parent change
|
|
3050
3459
|
* @param {string} currentContent - Current working copy content
|
|
3051
3460
|
* @param {string[]} startingPoints - Change IDs to start search from (parent changeIds)
|
|
3052
|
-
* @returns {Promise<Map
|
|
3461
|
+
* @returns {Promise<Map<any, any>>} Map of ancestorId -> { lines: Map(lineNum -> content), originalContent }
|
|
3053
3462
|
*/
|
|
3054
3463
|
async _findLineAncestors(filePath, parentContent, currentContent, startingPoints) {
|
|
3055
3464
|
if (!startingPoints || startingPoints.length === 0) {
|
|
@@ -3203,7 +3612,11 @@ export async function createJJ(options) {
|
|
|
3203
3612
|
continue;
|
|
3204
3613
|
}
|
|
3205
3614
|
|
|
3206
|
-
if (
|
|
3615
|
+
if (
|
|
3616
|
+
change.parents &&
|
|
3617
|
+
change.parents.includes(currentId) &&
|
|
3618
|
+
!visited.has(change.changeId)
|
|
3619
|
+
) {
|
|
3207
3620
|
visited.add(change.changeId);
|
|
3208
3621
|
queue.push(change.changeId);
|
|
3209
3622
|
descendants.push(change.changeId);
|
|
@@ -3219,11 +3632,16 @@ export async function createJJ(options) {
|
|
|
3219
3632
|
*
|
|
3220
3633
|
* @private
|
|
3221
3634
|
* @param {string[]} descendantIds - List of descendant IDs to rebuild
|
|
3222
|
-
* @param {Map} originalStates - Map of changeId -> original state before absorb
|
|
3635
|
+
* @param {Map<any, any>} originalStates - Map of changeId -> original state before absorb
|
|
3223
3636
|
* @param {string} stopAtChangeId - Don't rebuild this change
|
|
3224
|
-
* @param {Set} directlyModified - Set of changeIds that were directly modified by absorb
|
|
3637
|
+
* @param {Set<any>} directlyModified - Set of changeIds that were directly modified by absorb
|
|
3225
3638
|
*/
|
|
3226
|
-
async _rebuildDescendantsWithStates(
|
|
3639
|
+
async _rebuildDescendantsWithStates(
|
|
3640
|
+
descendantIds,
|
|
3641
|
+
originalStates,
|
|
3642
|
+
stopAtChangeId,
|
|
3643
|
+
directlyModified = new Set()
|
|
3644
|
+
) {
|
|
3227
3645
|
// Sort descendants by depth (parents before children)
|
|
3228
3646
|
const sorted = await this._topologicalSort(descendantIds);
|
|
3229
3647
|
|
|
@@ -3269,6 +3687,7 @@ export async function createJJ(options) {
|
|
|
3269
3687
|
|
|
3270
3688
|
// For each file, track which lines the descendant modified from original parent
|
|
3271
3689
|
// then reapply those modifications to the updated parent
|
|
3690
|
+
/** @type {Record<string, any>} */
|
|
3272
3691
|
const newSnapshot = {};
|
|
3273
3692
|
|
|
3274
3693
|
const allFiles = new Set([
|
|
@@ -3290,9 +3709,15 @@ export async function createJJ(options) {
|
|
|
3290
3709
|
const resultLines = [...updatedParentLines];
|
|
3291
3710
|
|
|
3292
3711
|
// Find lines that descendant modified
|
|
3293
|
-
for (
|
|
3294
|
-
|
|
3295
|
-
|
|
3712
|
+
for (
|
|
3713
|
+
let i = 0;
|
|
3714
|
+
i < Math.max(originalDescendantLines.length, originalParentLines.length);
|
|
3715
|
+
i++
|
|
3716
|
+
) {
|
|
3717
|
+
const originalDescendantLine =
|
|
3718
|
+
i < originalDescendantLines.length ? originalDescendantLines[i] : undefined;
|
|
3719
|
+
const originalParentLine =
|
|
3720
|
+
i < originalParentLines.length ? originalParentLines[i] : undefined;
|
|
3296
3721
|
|
|
3297
3722
|
// If descendant modified this line from original parent, apply that modification
|
|
3298
3723
|
if (originalDescendantLine !== originalParentLine) {
|
|
@@ -3356,7 +3781,11 @@ export async function createJJ(options) {
|
|
|
3356
3781
|
continue;
|
|
3357
3782
|
}
|
|
3358
3783
|
|
|
3359
|
-
if (
|
|
3784
|
+
if (
|
|
3785
|
+
change.parents &&
|
|
3786
|
+
change.parents.includes(currentId) &&
|
|
3787
|
+
!visited.has(change.changeId)
|
|
3788
|
+
) {
|
|
3360
3789
|
visited.add(change.changeId);
|
|
3361
3790
|
queue.push(change.changeId);
|
|
3362
3791
|
descendants.push(change.changeId);
|
|
@@ -3394,6 +3823,7 @@ export async function createJJ(options) {
|
|
|
3394
3823
|
|
|
3395
3824
|
// For each file, track which lines the descendant modified from original parent
|
|
3396
3825
|
// then reapply those modifications to the updated parent
|
|
3826
|
+
/** @type {Record<string, any>} */
|
|
3397
3827
|
const newSnapshot = {};
|
|
3398
3828
|
|
|
3399
3829
|
const allFiles = new Set([
|
|
@@ -3417,7 +3847,8 @@ export async function createJJ(options) {
|
|
|
3417
3847
|
// Find lines that descendant modified
|
|
3418
3848
|
for (let i = 0; i < Math.max(descendantLines.length, originalParentLines.length); i++) {
|
|
3419
3849
|
const descendantLine = i < descendantLines.length ? descendantLines[i] : undefined;
|
|
3420
|
-
const originalParentLine =
|
|
3850
|
+
const originalParentLine =
|
|
3851
|
+
i < originalParentLines.length ? originalParentLines[i] : undefined;
|
|
3421
3852
|
|
|
3422
3853
|
// If descendant modified this line from original parent, apply that modification
|
|
3423
3854
|
if (descendantLine !== originalParentLine) {
|
|
@@ -3451,15 +3882,14 @@ export async function createJJ(options) {
|
|
|
3451
3882
|
* @returns {Promise<string[]>} Sorted change IDs
|
|
3452
3883
|
*/
|
|
3453
3884
|
async _topologicalSort(changeIds) {
|
|
3454
|
-
const changes = await Promise.all(
|
|
3455
|
-
changeIds.map(id => graph.getChange(id))
|
|
3456
|
-
);
|
|
3885
|
+
const changes = await Promise.all(changeIds.map((id) => graph.getChange(id)));
|
|
3457
3886
|
|
|
3887
|
+
/** @type {any[]} */
|
|
3458
3888
|
const sorted = [];
|
|
3459
3889
|
const visited = new Set();
|
|
3460
3890
|
const inProgress = new Set();
|
|
3461
3891
|
|
|
3462
|
-
const visit = async (change) => {
|
|
3892
|
+
const visit = async (/** @type {any} */ change) => {
|
|
3463
3893
|
if (!change || visited.has(change.changeId)) {
|
|
3464
3894
|
return;
|
|
3465
3895
|
}
|
|
@@ -3499,8 +3929,7 @@ export async function createJJ(options) {
|
|
|
3499
3929
|
* A parent is redundant if it's already an ancestor through another parent.
|
|
3500
3930
|
* This operation optimizes the graph structure without changing the content.
|
|
3501
3931
|
*
|
|
3502
|
-
* @param {
|
|
3503
|
-
* @param {string} args.revision - Change ID to simplify
|
|
3932
|
+
* @param {Record<string, any>} args - Arguments
|
|
3504
3933
|
* @returns {Promise<{changeId: string, simplified: boolean, removedParents?: string[]}>}
|
|
3505
3934
|
*/
|
|
3506
3935
|
async simplifyParents(args) {
|
|
@@ -3514,7 +3943,7 @@ export async function createJJ(options) {
|
|
|
3514
3943
|
await userConfig.load();
|
|
3515
3944
|
|
|
3516
3945
|
const change = await graph.getChange(args.revision);
|
|
3517
|
-
const user = userConfig.getUser();
|
|
3946
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
3518
3947
|
|
|
3519
3948
|
if (!change) {
|
|
3520
3949
|
throw new JJError('CHANGE_NOT_FOUND', `Change ${args.revision} not found`);
|
|
@@ -3530,6 +3959,7 @@ export async function createJJ(options) {
|
|
|
3530
3959
|
|
|
3531
3960
|
// Find redundant parents
|
|
3532
3961
|
// A parent P is redundant if there exists another parent Q such that P is an ancestor of Q
|
|
3962
|
+
/** @type {any[]} */
|
|
3533
3963
|
const redundantParents = [];
|
|
3534
3964
|
|
|
3535
3965
|
for (let i = 0; i < change.parents.length; i++) {
|
|
@@ -3543,7 +3973,7 @@ export async function createJJ(options) {
|
|
|
3543
3973
|
|
|
3544
3974
|
// Get all ancestors of parentQ
|
|
3545
3975
|
const ancestorsQ = await this.log({ revisions: [parentQ] });
|
|
3546
|
-
const ancestorIds = ancestorsQ.map(c => c.changeId);
|
|
3976
|
+
const ancestorIds = ancestorsQ.map((/** @type {any} */ c) => c.changeId);
|
|
3547
3977
|
|
|
3548
3978
|
// If parentP is in the ancestors of parentQ, then parentP is redundant
|
|
3549
3979
|
if (ancestorIds.includes(parentP)) {
|
|
@@ -3564,7 +3994,9 @@ export async function createJJ(options) {
|
|
|
3564
3994
|
}
|
|
3565
3995
|
|
|
3566
3996
|
// Remove redundant parents
|
|
3567
|
-
change.parents = change.parents.filter(
|
|
3997
|
+
change.parents = change.parents.filter(
|
|
3998
|
+
(/** @type {any} */ p) => !redundantParents.includes(p)
|
|
3999
|
+
);
|
|
3568
4000
|
await graph.updateChange(change);
|
|
3569
4001
|
|
|
3570
4002
|
// Record operation
|
|
@@ -3591,8 +4023,7 @@ export async function createJJ(options) {
|
|
|
3591
4023
|
/**
|
|
3592
4024
|
* Un-abandon a change (restore from abandoned state)
|
|
3593
4025
|
*
|
|
3594
|
-
* @param {
|
|
3595
|
-
* @param {string} args.changeId - Change ID to un-abandon
|
|
4026
|
+
* @param {Record<string, any>} args - Arguments
|
|
3596
4027
|
* @returns {Promise<Object>} The un-abandoned change object including changeId, description, and abandoned flag (now false)
|
|
3597
4028
|
*/
|
|
3598
4029
|
async unabandon(args) {
|
|
@@ -3617,7 +4048,7 @@ export async function createJJ(options) {
|
|
|
3617
4048
|
await userConfig.load();
|
|
3618
4049
|
|
|
3619
4050
|
const change = await graph.getChange(args.changeId);
|
|
3620
|
-
const user = userConfig.getUser();
|
|
4051
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
3621
4052
|
|
|
3622
4053
|
if (!change) {
|
|
3623
4054
|
throw new JJError('CHANGE_NOT_FOUND', `Change ${args.changeId} not found`);
|
|
@@ -3649,24 +4080,25 @@ export async function createJJ(options) {
|
|
|
3649
4080
|
});
|
|
3650
4081
|
|
|
3651
4082
|
// Dispatch change:unabandoned event (informational)
|
|
3652
|
-
await dispatchEventAsync(
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
4083
|
+
await dispatchEventAsync(
|
|
4084
|
+
jj,
|
|
4085
|
+
'change:unabandoned',
|
|
4086
|
+
{
|
|
4087
|
+
operation: 'unabandon',
|
|
4088
|
+
changeId: args.changeId,
|
|
4089
|
+
change,
|
|
4090
|
+
timestamp: new Date().toISOString(),
|
|
4091
|
+
},
|
|
4092
|
+
{ cancelable: false }
|
|
4093
|
+
);
|
|
3658
4094
|
|
|
3659
4095
|
return change;
|
|
3660
4096
|
},
|
|
3661
|
-
|
|
4097
|
+
|
|
3662
4098
|
/**
|
|
3663
4099
|
* Split a change into multiple changes (simplified v0.2 implementation)
|
|
3664
4100
|
*
|
|
3665
|
-
* @param {
|
|
3666
|
-
* @param {string} args.changeId - Change ID to split
|
|
3667
|
-
* @param {string} args.description1 - Description for first part
|
|
3668
|
-
* @param {string} args.description2 - Description for second part
|
|
3669
|
-
* @param {string[]} [args.paths] - Specific paths to include in first split (full implementation planned for future)
|
|
4101
|
+
* @param {Record<string, any>} args - Arguments
|
|
3670
4102
|
*/
|
|
3671
4103
|
async split(args) {
|
|
3672
4104
|
// Normalize: accept common parameter aliases
|
|
@@ -3746,7 +4178,7 @@ export async function createJJ(options) {
|
|
|
3746
4178
|
|
|
3747
4179
|
return { original: originalChange, new: newChange };
|
|
3748
4180
|
},
|
|
3749
|
-
|
|
4181
|
+
|
|
3750
4182
|
// ========================================
|
|
3751
4183
|
// v0.3 FEATURES: Git Integration
|
|
3752
4184
|
// ========================================
|
|
@@ -3759,18 +4191,13 @@ export async function createJJ(options) {
|
|
|
3759
4191
|
* Initialize a new Git-backed JJ repository
|
|
3760
4192
|
* Matches `jj git init` CLI command
|
|
3761
4193
|
*
|
|
3762
|
-
* @param {
|
|
3763
|
-
* @param {string} [opts.userName] - User name for commits
|
|
3764
|
-
* @param {string} [opts.userEmail] - User email for commits
|
|
3765
|
-
* @param {string} [opts.defaultBranch] - Default branch name (default: 'main')
|
|
4194
|
+
* @param {Record<string, any>} opts - Initialization options
|
|
3766
4195
|
*/
|
|
3767
4196
|
async init(opts = {}) {
|
|
3768
4197
|
if (!gitBackend) {
|
|
3769
|
-
throw new JJError(
|
|
3770
|
-
'
|
|
3771
|
-
|
|
3772
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
3773
|
-
);
|
|
4198
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4199
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4200
|
+
});
|
|
3774
4201
|
}
|
|
3775
4202
|
|
|
3776
4203
|
// Initialize Git backend
|
|
@@ -3795,7 +4222,7 @@ export async function createJJ(options) {
|
|
|
3795
4222
|
|
|
3796
4223
|
// Create root change
|
|
3797
4224
|
const rootChangeId = generateChangeId();
|
|
3798
|
-
const user = userConfig.getUser();
|
|
4225
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
3799
4226
|
const rootChange = {
|
|
3800
4227
|
changeId: rootChangeId,
|
|
3801
4228
|
commitId: '0000000000000000000000000000000000000000',
|
|
@@ -3840,32 +4267,22 @@ export async function createJJ(options) {
|
|
|
3840
4267
|
/**
|
|
3841
4268
|
* Fetch changes from remote Git repository
|
|
3842
4269
|
*
|
|
3843
|
-
* @param {
|
|
3844
|
-
* @param {string} args.remote - Remote name or URL
|
|
3845
|
-
* @param {string[]} [args.refs] - Refs to fetch (default: all)
|
|
3846
|
-
* @param {number} [args.depth] - Create shallow clone with history truncated to depth (v0.4)
|
|
3847
|
-
* @param {boolean} [args.relative] - Depth measured from current shallow depth (v0.4)
|
|
3848
|
-
* @param {boolean} [args.singleBranch] - Only fetch single branch (v0.4)
|
|
3849
|
-
* @param {boolean} [args.noTags] - Don't fetch tags (v0.4)
|
|
3850
|
-
* @param {Function} [args.onProgress] - Progress callback
|
|
3851
|
-
* @param {Function} [args.onAuth] - Authentication callback
|
|
4270
|
+
* @param {Record<string, any>} args - Fetch arguments
|
|
3852
4271
|
*/
|
|
3853
4272
|
async fetch(args) {
|
|
3854
4273
|
if (!gitBackend) {
|
|
3855
|
-
throw new JJError(
|
|
3856
|
-
'
|
|
3857
|
-
|
|
3858
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
3859
|
-
);
|
|
4274
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4275
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4276
|
+
});
|
|
3860
4277
|
}
|
|
3861
4278
|
|
|
3862
4279
|
const result = await gitBackend.fetch({
|
|
3863
4280
|
remote: args.remote,
|
|
3864
4281
|
refs: args.refs,
|
|
3865
|
-
depth: args.depth,
|
|
3866
|
-
relative: args.relative,
|
|
4282
|
+
depth: args.depth, // v0.4: Shallow clone support
|
|
4283
|
+
relative: args.relative, // v0.4: Relative depth
|
|
3867
4284
|
singleBranch: args.singleBranch, // v0.4: Single branch only
|
|
3868
|
-
noTags: args.noTags,
|
|
4285
|
+
noTags: args.noTags, // v0.4: Skip tags
|
|
3869
4286
|
onProgress: args.onProgress,
|
|
3870
4287
|
onAuth: args.onAuth,
|
|
3871
4288
|
});
|
|
@@ -3890,20 +4307,13 @@ export async function createJJ(options) {
|
|
|
3890
4307
|
/**
|
|
3891
4308
|
* Push changes to remote Git repository
|
|
3892
4309
|
*
|
|
3893
|
-
* @param {
|
|
3894
|
-
* @param {string} args.remote - Remote name or URL
|
|
3895
|
-
* @param {string[]} [args.refs] - Refs to push (default: current bookmarks)
|
|
3896
|
-
* @param {boolean} [args.force] - Allow non-fast-forward
|
|
3897
|
-
* @param {Function} [args.onProgress] - Progress callback
|
|
3898
|
-
* @param {Function} [args.onAuth] - Authentication callback
|
|
4310
|
+
* @param {Record<string, any>} args - Push arguments
|
|
3899
4311
|
*/
|
|
3900
4312
|
async push(args) {
|
|
3901
4313
|
if (!gitBackend) {
|
|
3902
|
-
throw new JJError(
|
|
3903
|
-
'
|
|
3904
|
-
|
|
3905
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
3906
|
-
);
|
|
4314
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4315
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4316
|
+
});
|
|
3907
4317
|
}
|
|
3908
4318
|
|
|
3909
4319
|
const result = await gitBackend.push({
|
|
@@ -3934,24 +4344,14 @@ export async function createJJ(options) {
|
|
|
3934
4344
|
/**
|
|
3935
4345
|
* Clone a Git repository (matches `jj git clone`)
|
|
3936
4346
|
*
|
|
3937
|
-
* @param {
|
|
3938
|
-
* @param {string} args.url - Repository URL to clone from
|
|
3939
|
-
* @param {string} [args.dir] - Directory to clone into (defaults to repo name from URL)
|
|
3940
|
-
* @param {number} [args.depth] - Create shallow clone with history truncated to depth
|
|
3941
|
-
* @param {boolean} [args.singleBranch] - Only clone single branch
|
|
3942
|
-
* @param {boolean} [args.noTags] - Don't clone tags
|
|
3943
|
-
* @param {string} [args.ref] - Specific ref/branch to clone
|
|
3944
|
-
* @param {Function} [args.onProgress] - Progress callback
|
|
3945
|
-
* @param {Function} [args.onAuth] - Authentication callback
|
|
4347
|
+
* @param {Record<string, any>} args - Clone arguments
|
|
3946
4348
|
* @returns {Promise<Object>} Clone result with directory path
|
|
3947
4349
|
*/
|
|
3948
4350
|
async clone(args) {
|
|
3949
4351
|
if (!gitBackend) {
|
|
3950
|
-
throw new JJError(
|
|
3951
|
-
'
|
|
3952
|
-
|
|
3953
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
3954
|
-
);
|
|
4352
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4353
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4354
|
+
});
|
|
3955
4355
|
}
|
|
3956
4356
|
|
|
3957
4357
|
if (!args || !args.url) {
|
|
@@ -3968,7 +4368,12 @@ export async function createJJ(options) {
|
|
|
3968
4368
|
|
|
3969
4369
|
// Determine clone directory
|
|
3970
4370
|
const git = (await import('isomorphic-git')).default;
|
|
3971
|
-
const cloneDir =
|
|
4371
|
+
const cloneDir =
|
|
4372
|
+
args.dir ||
|
|
4373
|
+
args.url
|
|
4374
|
+
.split('/')
|
|
4375
|
+
.pop()
|
|
4376
|
+
.replace(/\.git$/, '');
|
|
3972
4377
|
const fullCloneDir = path.join(dir, cloneDir);
|
|
3973
4378
|
|
|
3974
4379
|
// Clone using isomorphic-git
|
|
@@ -3986,7 +4391,9 @@ export async function createJJ(options) {
|
|
|
3986
4391
|
});
|
|
3987
4392
|
|
|
3988
4393
|
// Initialize JJ repository structure in cloned directory
|
|
3989
|
-
const clonedBackend =
|
|
4394
|
+
const clonedBackend = /** @type {any} */ (
|
|
4395
|
+
new IsomorphicGitBackend({ fs, http, dir: fullCloneDir })
|
|
4396
|
+
);
|
|
3990
4397
|
await clonedBackend._createJJRepoStructure();
|
|
3991
4398
|
|
|
3992
4399
|
// Record operation
|
|
@@ -4017,11 +4424,9 @@ export async function createJJ(options) {
|
|
|
4017
4424
|
*/
|
|
4018
4425
|
async import() {
|
|
4019
4426
|
if (!gitBackend) {
|
|
4020
|
-
throw new JJError(
|
|
4021
|
-
'
|
|
4022
|
-
|
|
4023
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
4024
|
-
);
|
|
4427
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4428
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4429
|
+
});
|
|
4025
4430
|
}
|
|
4026
4431
|
|
|
4027
4432
|
await graph.load();
|
|
@@ -4039,14 +4444,18 @@ export async function createJJ(options) {
|
|
|
4039
4444
|
if (!change) {
|
|
4040
4445
|
// Create a new change for this commit
|
|
4041
4446
|
const changeId = generateChangeId();
|
|
4042
|
-
const user = userConfig.getUser();
|
|
4447
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
4043
4448
|
change = {
|
|
4044
4449
|
changeId,
|
|
4045
4450
|
commitId: ref.oid,
|
|
4046
4451
|
parents: [],
|
|
4047
4452
|
tree: ref.oid,
|
|
4048
4453
|
author: { name: user.name, email: user.email, timestamp: new Date().toISOString() },
|
|
4049
|
-
committer: {
|
|
4454
|
+
committer: {
|
|
4455
|
+
name: user.name,
|
|
4456
|
+
email: user.email,
|
|
4457
|
+
timestamp: new Date().toISOString(),
|
|
4458
|
+
},
|
|
4050
4459
|
description: `Imported from Git ref ${ref.name}`,
|
|
4051
4460
|
timestamp: new Date().toISOString(),
|
|
4052
4461
|
};
|
|
@@ -4090,11 +4499,9 @@ export async function createJJ(options) {
|
|
|
4090
4499
|
*/
|
|
4091
4500
|
async export() {
|
|
4092
4501
|
if (!gitBackend) {
|
|
4093
|
-
throw new JJError(
|
|
4094
|
-
'
|
|
4095
|
-
|
|
4096
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
4097
|
-
);
|
|
4502
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4503
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4504
|
+
});
|
|
4098
4505
|
}
|
|
4099
4506
|
|
|
4100
4507
|
await graph.load();
|
|
@@ -4139,37 +4546,31 @@ export async function createJJ(options) {
|
|
|
4139
4546
|
/**
|
|
4140
4547
|
* List Git remotes (matches `jj git remote list`)
|
|
4141
4548
|
*
|
|
4142
|
-
* @returns {Promise<Array
|
|
4549
|
+
* @returns {Promise<Array<any>>} List of remotes with names and URLs
|
|
4143
4550
|
*/
|
|
4144
4551
|
async list() {
|
|
4145
4552
|
if (!gitBackend) {
|
|
4146
|
-
throw new JJError(
|
|
4147
|
-
'
|
|
4148
|
-
|
|
4149
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
4150
|
-
);
|
|
4553
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4554
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4555
|
+
});
|
|
4151
4556
|
}
|
|
4152
4557
|
|
|
4153
4558
|
const git = (await import('isomorphic-git')).default;
|
|
4154
4559
|
const remotes = await git.listRemotes({ fs, dir });
|
|
4155
|
-
return remotes.map(r => ({ name: r.remote, url: r.url }));
|
|
4560
|
+
return remotes.map((r) => ({ name: r.remote, url: r.url }));
|
|
4156
4561
|
},
|
|
4157
4562
|
|
|
4158
4563
|
/**
|
|
4159
4564
|
* Add a Git remote (matches `jj git remote add`)
|
|
4160
4565
|
*
|
|
4161
|
-
* @param {
|
|
4162
|
-
* @param {string} args.name - Remote name
|
|
4163
|
-
* @param {string} args.url - Remote URL
|
|
4566
|
+
* @param {Record<string, any>} args - Remote arguments
|
|
4164
4567
|
* @returns {Promise<Object>} Added remote info
|
|
4165
4568
|
*/
|
|
4166
4569
|
async add(args) {
|
|
4167
4570
|
if (!gitBackend) {
|
|
4168
|
-
throw new JJError(
|
|
4169
|
-
'
|
|
4170
|
-
|
|
4171
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
4172
|
-
);
|
|
4571
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4572
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4573
|
+
});
|
|
4173
4574
|
}
|
|
4174
4575
|
|
|
4175
4576
|
if (!args || !args.name || !args.url) {
|
|
@@ -4206,17 +4607,14 @@ export async function createJJ(options) {
|
|
|
4206
4607
|
/**
|
|
4207
4608
|
* Remove a Git remote (matches `jj git remote remove`)
|
|
4208
4609
|
*
|
|
4209
|
-
* @param {
|
|
4210
|
-
* @param {string} args.name - Remote name to remove
|
|
4610
|
+
* @param {Record<string, any>} args - Remote arguments
|
|
4211
4611
|
* @returns {Promise<Object>} Removal result
|
|
4212
4612
|
*/
|
|
4213
4613
|
async remove(args) {
|
|
4214
4614
|
if (!gitBackend) {
|
|
4215
|
-
throw new JJError(
|
|
4216
|
-
'
|
|
4217
|
-
|
|
4218
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
4219
|
-
);
|
|
4615
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4616
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4617
|
+
});
|
|
4220
4618
|
}
|
|
4221
4619
|
|
|
4222
4620
|
if (!args || !args.name) {
|
|
@@ -4252,18 +4650,14 @@ export async function createJJ(options) {
|
|
|
4252
4650
|
/**
|
|
4253
4651
|
* Rename a Git remote (matches `jj git remote rename`)
|
|
4254
4652
|
*
|
|
4255
|
-
* @param {
|
|
4256
|
-
* @param {string} args.oldName - Current remote name
|
|
4257
|
-
* @param {string} args.newName - New remote name
|
|
4653
|
+
* @param {Record<string, any>} args - Remote arguments
|
|
4258
4654
|
* @returns {Promise<Object>} Rename result
|
|
4259
4655
|
*/
|
|
4260
4656
|
async rename(args) {
|
|
4261
4657
|
if (!gitBackend) {
|
|
4262
|
-
throw new JJError(
|
|
4263
|
-
'
|
|
4264
|
-
|
|
4265
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
4266
|
-
);
|
|
4658
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4659
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4660
|
+
});
|
|
4267
4661
|
}
|
|
4268
4662
|
|
|
4269
4663
|
if (!args || !args.oldName || !args.newName) {
|
|
@@ -4276,7 +4670,7 @@ export async function createJJ(options) {
|
|
|
4276
4670
|
|
|
4277
4671
|
// Get current URL
|
|
4278
4672
|
const remotes = await git.listRemotes({ fs, dir });
|
|
4279
|
-
const remote = remotes.find(r => r.remote === args.oldName);
|
|
4673
|
+
const remote = remotes.find((r) => r.remote === args.oldName);
|
|
4280
4674
|
if (!remote) {
|
|
4281
4675
|
throw new JJError('NOT_FOUND', `Remote ${args.oldName} not found`);
|
|
4282
4676
|
}
|
|
@@ -4305,18 +4699,14 @@ export async function createJJ(options) {
|
|
|
4305
4699
|
/**
|
|
4306
4700
|
* Set URL for a Git remote (matches `jj git remote set-url`)
|
|
4307
4701
|
*
|
|
4308
|
-
* @param {
|
|
4309
|
-
* @param {string} args.name - Remote name
|
|
4310
|
-
* @param {string} args.url - New URL
|
|
4702
|
+
* @param {Record<string, any>} args - Remote arguments
|
|
4311
4703
|
* @returns {Promise<Object>} Updated remote info
|
|
4312
4704
|
*/
|
|
4313
4705
|
async setUrl(args) {
|
|
4314
4706
|
if (!gitBackend) {
|
|
4315
|
-
throw new JJError(
|
|
4316
|
-
'
|
|
4317
|
-
|
|
4318
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
4319
|
-
);
|
|
4707
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4708
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4709
|
+
});
|
|
4320
4710
|
}
|
|
4321
4711
|
|
|
4322
4712
|
if (!args || !args.name || !args.url) {
|
|
@@ -4329,7 +4719,7 @@ export async function createJJ(options) {
|
|
|
4329
4719
|
|
|
4330
4720
|
// Check if remote exists
|
|
4331
4721
|
const remotes = await git.listRemotes({ fs, dir });
|
|
4332
|
-
const remote = remotes.find(r => r.remote === args.name);
|
|
4722
|
+
const remote = remotes.find((r) => r.remote === args.name);
|
|
4333
4723
|
if (!remote) {
|
|
4334
4724
|
throw new JJError('NOT_FOUND', `Remote ${args.name} not found`);
|
|
4335
4725
|
}
|
|
@@ -4383,8 +4773,7 @@ export async function createJJ(options) {
|
|
|
4383
4773
|
/**
|
|
4384
4774
|
* Merge changes from another change, detecting conflicts
|
|
4385
4775
|
*
|
|
4386
|
-
* @param {
|
|
4387
|
-
* @param {string} args.source - Source change ID to merge
|
|
4776
|
+
* @param {Record<string, any>} args - Merge arguments
|
|
4388
4777
|
* @returns {Promise<Object>} Merge result with conflicts
|
|
4389
4778
|
*/
|
|
4390
4779
|
async merge(args) {
|
|
@@ -4402,7 +4791,7 @@ export async function createJJ(options) {
|
|
|
4402
4791
|
};
|
|
4403
4792
|
|
|
4404
4793
|
const currentChangeId = workingCopy.getCurrentChangeId();
|
|
4405
|
-
const
|
|
4794
|
+
const _currentChange = await graph.getChange(currentChangeId);
|
|
4406
4795
|
const sourceChange = await graph.getChange(args.source);
|
|
4407
4796
|
|
|
4408
4797
|
if (!sourceChange) {
|
|
@@ -4467,9 +4856,9 @@ export async function createJJ(options) {
|
|
|
4467
4856
|
baseFiles,
|
|
4468
4857
|
leftFiles,
|
|
4469
4858
|
rightFiles,
|
|
4470
|
-
drivers: args.drivers || {},
|
|
4471
|
-
workingCopyDir: args.dryRun ? null : dir,
|
|
4472
|
-
baseChange: baseChangeId,
|
|
4859
|
+
drivers: args.drivers || {}, // v0.5: custom merge drivers
|
|
4860
|
+
workingCopyDir: /** @type {any} */ (args.dryRun ? null : dir), // v0.5: skip file writes in dry-run
|
|
4861
|
+
baseChange: baseChangeId, // v0.5: metadata for drivers
|
|
4473
4862
|
leftChange: currentChangeId,
|
|
4474
4863
|
rightChange: args.source,
|
|
4475
4864
|
});
|
|
@@ -4522,8 +4911,7 @@ export async function createJJ(options) {
|
|
|
4522
4911
|
/**
|
|
4523
4912
|
* List active (unresolved) conflicts
|
|
4524
4913
|
*
|
|
4525
|
-
* @param {
|
|
4526
|
-
* @param {boolean} [opts.includeResolved=false] - Include resolved conflicts
|
|
4914
|
+
* @param {Record<string, any>} opts - List options
|
|
4527
4915
|
*/
|
|
4528
4916
|
async list(opts = {}) {
|
|
4529
4917
|
await conflicts.load();
|
|
@@ -4535,11 +4923,7 @@ export async function createJJ(options) {
|
|
|
4535
4923
|
/**
|
|
4536
4924
|
* Resolve a conflict (v0.5 enhanced)
|
|
4537
4925
|
*
|
|
4538
|
-
* @param {
|
|
4539
|
-
* @param {string} args.conflictId - Conflict ID
|
|
4540
|
-
* @param {string} [args.resolution] - Manual resolution content
|
|
4541
|
-
* @param {string} [args.driver] - Merge driver to use
|
|
4542
|
-
* @param {string} [args.strategy] - Resolution strategy ('ours', 'theirs', 'union')
|
|
4926
|
+
* @param {Record<string, any>} args - Resolution arguments
|
|
4543
4927
|
*/
|
|
4544
4928
|
async resolve(args) {
|
|
4545
4929
|
if (!args || !args.conflictId) {
|
|
@@ -4569,12 +4953,12 @@ export async function createJJ(options) {
|
|
|
4569
4953
|
|
|
4570
4954
|
const driverResult = await mergeDrivers.executeDriver(
|
|
4571
4955
|
driver,
|
|
4572
|
-
{
|
|
4956
|
+
/** @type {any} */ ({
|
|
4573
4957
|
path: conflict.path,
|
|
4574
4958
|
base: conflict.sides.base,
|
|
4575
4959
|
ours: conflict.sides.left,
|
|
4576
4960
|
theirs: conflict.sides.right,
|
|
4577
|
-
},
|
|
4961
|
+
}),
|
|
4578
4962
|
mergeDrivers.isBinaryFile(conflict.path, conflict.sides.left)
|
|
4579
4963
|
);
|
|
4580
4964
|
|
|
@@ -4603,10 +4987,7 @@ export async function createJJ(options) {
|
|
|
4603
4987
|
/**
|
|
4604
4988
|
* Resolve all conflicts (v0.5)
|
|
4605
4989
|
*
|
|
4606
|
-
* @param {
|
|
4607
|
-
* @param {string} args.strategy - Resolution strategy ('ours', 'theirs', 'union')
|
|
4608
|
-
* @param {Object} [args.filter] - Optional filter
|
|
4609
|
-
* @param {string} [args.filter.path] - Path pattern to filter
|
|
4990
|
+
* @param {Record<string, any>} args - Resolution arguments
|
|
4610
4991
|
*/
|
|
4611
4992
|
async resolveAll(args) {
|
|
4612
4993
|
if (!args || !args.strategy) {
|
|
@@ -4620,7 +5001,7 @@ export async function createJJ(options) {
|
|
|
4620
5001
|
let toResolve = allConflicts;
|
|
4621
5002
|
if (args.filter && args.filter.path) {
|
|
4622
5003
|
const pattern = args.filter.path;
|
|
4623
|
-
toResolve = allConflicts.filter(c => {
|
|
5004
|
+
toResolve = allConflicts.filter((c) => {
|
|
4624
5005
|
// Simple pattern matching - exact match or glob
|
|
4625
5006
|
if (pattern.includes('*')) {
|
|
4626
5007
|
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
@@ -4655,8 +5036,7 @@ export async function createJJ(options) {
|
|
|
4655
5036
|
/**
|
|
4656
5037
|
* Get conflict markers for a conflict (v0.5)
|
|
4657
5038
|
*
|
|
4658
|
-
* @param {
|
|
4659
|
-
* @param {string} args.conflictId - Conflict ID
|
|
5039
|
+
* @param {Record<string, any>} args - Arguments
|
|
4660
5040
|
* @returns {Promise<string>} Conflict markers in standard format
|
|
4661
5041
|
*/
|
|
4662
5042
|
async markers(args) {
|
|
@@ -4671,17 +5051,24 @@ export async function createJJ(options) {
|
|
|
4671
5051
|
throw new JJError('CONFLICT_NOT_FOUND', `Conflict ${args.conflictId} not found`);
|
|
4672
5052
|
}
|
|
4673
5053
|
|
|
4674
|
-
// Generate standard conflict markers
|
|
5054
|
+
// Generate standard conflict markers. Force a newline before each
|
|
5055
|
+
// boundary marker whenever there's non-empty content that doesn't
|
|
5056
|
+
// already end in one — otherwise the content fuses onto the
|
|
5057
|
+
// `=======`/`>>>>>>> theirs` marker, producing a corrupted/unparseable
|
|
5058
|
+
// line (see issue #15). Empty sides (e.g. a modify-delete conflict)
|
|
5059
|
+
// intentionally get no extra blank line before the marker.
|
|
4675
5060
|
const ours = conflict.sides.left || '';
|
|
4676
5061
|
const theirs = conflict.sides.right || '';
|
|
5062
|
+
const oursSep = ours && !ours.endsWith('\n') ? '\n' : '';
|
|
5063
|
+
const theirsSep = theirs && !theirs.endsWith('\n') ? '\n' : '';
|
|
4677
5064
|
|
|
4678
|
-
return `<<<<<<< ours\n${ours}=======\n${theirs}>>>>>>> theirs`;
|
|
5065
|
+
return `<<<<<<< ours\n${ours}${oursSep}=======\n${theirs}${theirsSep}>>>>>>> theirs`;
|
|
4679
5066
|
},
|
|
4680
5067
|
|
|
4681
5068
|
/**
|
|
4682
5069
|
* Mark conflict as resolved
|
|
4683
5070
|
*/
|
|
4684
|
-
async markResolved(args) {
|
|
5071
|
+
async markResolved(/** @type {Record<string, any>} */ args) {
|
|
4685
5072
|
if (!args || !args.conflictId) {
|
|
4686
5073
|
throw new JJError('INVALID_ARGUMENT', 'Missing conflictId');
|
|
4687
5074
|
}
|
|
@@ -4702,19 +5089,15 @@ export async function createJJ(options) {
|
|
|
4702
5089
|
*
|
|
4703
5090
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
4704
5091
|
*/
|
|
4705
|
-
async resolve(args = {}) {
|
|
5092
|
+
async resolve(/** @type {Record<string, any>} */ args = {}) {
|
|
4706
5093
|
// Check for external merge tool usage
|
|
4707
5094
|
if (args.tool) {
|
|
4708
|
-
throw new JJError(
|
|
4709
|
-
'
|
|
4710
|
-
'External merge tools not
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
alternative: 'Use jj.conflicts.resolve() with programmatic resolution',
|
|
4715
|
-
suggestion: 'For conflict resolution, use the programmatic API or resolve files manually',
|
|
4716
|
-
}
|
|
4717
|
-
);
|
|
5095
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'External merge tools not supported', {
|
|
5096
|
+
feature: 'resolve --tool',
|
|
5097
|
+
reason: 'External merge tools require system integration not available in library API',
|
|
5098
|
+
alternative: 'Use jj.conflicts.resolve() with programmatic resolution',
|
|
5099
|
+
suggestion: 'For conflict resolution, use the programmatic API or resolve files manually',
|
|
5100
|
+
});
|
|
4718
5101
|
}
|
|
4719
5102
|
|
|
4720
5103
|
throw new JJError(
|
|
@@ -4743,10 +5126,7 @@ export async function createJJ(options) {
|
|
|
4743
5126
|
/**
|
|
4744
5127
|
* Show file content (matches `jj file show`)
|
|
4745
5128
|
*
|
|
4746
|
-
* @param {
|
|
4747
|
-
* @param {string} args.path - File path to read
|
|
4748
|
-
* @param {string} [args.changeId] - Change ID (defaults to working copy)
|
|
4749
|
-
* @param {string} [args.encoding='utf-8'] - Encoding ('utf-8' or 'binary')
|
|
5129
|
+
* @param {Record<string, any>} args - Arguments
|
|
4750
5130
|
* @returns {Promise<string|Uint8Array>} File contents
|
|
4751
5131
|
*/
|
|
4752
5132
|
async show(args) {
|
|
@@ -4756,20 +5136,91 @@ export async function createJJ(options) {
|
|
|
4756
5136
|
/**
|
|
4757
5137
|
* List files (matches `jj file list`)
|
|
4758
5138
|
*
|
|
4759
|
-
* @param {
|
|
4760
|
-
* @param {string} [args.changeId] - Change ID (defaults to working copy)
|
|
5139
|
+
* @param {Record<string, any>} [args={}] - Arguments
|
|
4761
5140
|
* @returns {Promise<Array<string>>} Array of file paths
|
|
4762
5141
|
*/
|
|
4763
5142
|
async list(args = {}) {
|
|
4764
5143
|
return await jj.listFiles(args);
|
|
4765
5144
|
},
|
|
4766
5145
|
|
|
5146
|
+
/**
|
|
5147
|
+
* Search file contents (matches `jj file search`, jj v0.37).
|
|
5148
|
+
*
|
|
5149
|
+
* Searches the tracked files of a change (defaults to the working copy)
|
|
5150
|
+
* for lines matching a pattern. Following jj v0.41, the pattern is treated
|
|
5151
|
+
* as a regular expression by default; pass `{ kind: 'substring' }` for a
|
|
5152
|
+
* literal search. isomorphic-jj has always returned the matched line
|
|
5153
|
+
* (and its 1-based `lineNumber`) alongside the path, which is what jj
|
|
5154
|
+
* v0.44 made the CLI's new default (`jj file search` used to print only
|
|
5155
|
+
* file paths). Pass `{ nameOnly: true }` for the pre-v0.44 CLI
|
|
5156
|
+
* behavior — a deduplicated list of matching file paths, matching the
|
|
5157
|
+
* new `--name-only` flag.
|
|
5158
|
+
*
|
|
5159
|
+
* @param {Record<string, any>} args - Arguments. Pass `nameOnly: true`
|
|
5160
|
+
* to return only unique matching file paths instead of per-line
|
|
5161
|
+
* matches (jj v0.44 `--name-only`)
|
|
5162
|
+
* @returns {Promise<Array<{path: string, lineNumber: number, line: string}>|Array<string>>}
|
|
5163
|
+
*/
|
|
5164
|
+
async search(args) {
|
|
5165
|
+
if (!args || !args.pattern) {
|
|
5166
|
+
throw new JJError('INVALID_ARGUMENT', 'Missing pattern argument', {
|
|
5167
|
+
suggestion: 'Provide a search pattern: { pattern: "TODO" }',
|
|
5168
|
+
});
|
|
5169
|
+
}
|
|
5170
|
+
|
|
5171
|
+
// v1.6: when searching the working copy, snapshot the disk first so
|
|
5172
|
+
// out-of-band files are searchable.
|
|
5173
|
+
if (!args.changeId && !args.change) {
|
|
5174
|
+
await autoSnapshotWorkingCopy();
|
|
5175
|
+
}
|
|
5176
|
+
|
|
5177
|
+
await graph.load();
|
|
5178
|
+
const changeId = args.changeId || args.change || workingCopy.getCurrentChangeId();
|
|
5179
|
+
const change = await graph.getChange(changeId);
|
|
5180
|
+
if (!change) {
|
|
5181
|
+
throw new JJError('CHANGE_NOT_FOUND', `Change ${changeId} not found`);
|
|
5182
|
+
}
|
|
5183
|
+
|
|
5184
|
+
const kind = args.kind || 'regex';
|
|
5185
|
+
let matcher;
|
|
5186
|
+
if (kind === 'substring') {
|
|
5187
|
+
matcher = (/** @type {any} */ line) => line.includes(args.pattern);
|
|
5188
|
+
} else {
|
|
5189
|
+
let regex;
|
|
5190
|
+
try {
|
|
5191
|
+
regex = new RegExp(args.pattern);
|
|
5192
|
+
} catch (error) {
|
|
5193
|
+
throw new JJError('INVALID_PATTERN', `Invalid regex pattern: ${error.message}`, {
|
|
5194
|
+
pattern: args.pattern,
|
|
5195
|
+
});
|
|
5196
|
+
}
|
|
5197
|
+
matcher = (/** @type {any} */ line) => regex.test(line);
|
|
5198
|
+
}
|
|
5199
|
+
|
|
5200
|
+
const snapshot = change.fileSnapshot || {};
|
|
5201
|
+
/** @type {any[]} */
|
|
5202
|
+
const results = [];
|
|
5203
|
+
for (const [filePath, content] of Object.entries(snapshot)) {
|
|
5204
|
+
if (args.path && filePath !== args.path) continue;
|
|
5205
|
+
if (typeof content !== 'string') continue; // skip binary
|
|
5206
|
+
const lines = content.split('\n');
|
|
5207
|
+
lines.forEach((line, index) => {
|
|
5208
|
+
if (matcher(line)) {
|
|
5209
|
+
results.push({ path: filePath, lineNumber: index + 1, line });
|
|
5210
|
+
}
|
|
5211
|
+
});
|
|
5212
|
+
}
|
|
5213
|
+
|
|
5214
|
+
if (args.nameOnly) {
|
|
5215
|
+
return Array.from(new Set(results.map((r) => r.path)));
|
|
5216
|
+
}
|
|
5217
|
+
return results;
|
|
5218
|
+
},
|
|
5219
|
+
|
|
4767
5220
|
/**
|
|
4768
5221
|
* Write file content (organized file operation)
|
|
4769
5222
|
*
|
|
4770
|
-
* @param {
|
|
4771
|
-
* @param {string} args.path - File path to write
|
|
4772
|
-
* @param {string|Uint8Array} args.data - File content
|
|
5223
|
+
* @param {Record<string, any>} args - Arguments
|
|
4773
5224
|
* @returns {Promise<Object>} Write result with file info
|
|
4774
5225
|
*/
|
|
4775
5226
|
async write(args) {
|
|
@@ -4779,9 +5230,7 @@ export async function createJJ(options) {
|
|
|
4779
5230
|
/**
|
|
4780
5231
|
* Move/rename file (organized file operation)
|
|
4781
5232
|
*
|
|
4782
|
-
* @param {
|
|
4783
|
-
* @param {string} args.from - Source path
|
|
4784
|
-
* @param {string} args.to - Destination path
|
|
5233
|
+
* @param {Record<string, any>} args - Arguments
|
|
4785
5234
|
* @returns {Promise<Object>} Move result
|
|
4786
5235
|
*/
|
|
4787
5236
|
async move(args) {
|
|
@@ -4791,8 +5240,7 @@ export async function createJJ(options) {
|
|
|
4791
5240
|
/**
|
|
4792
5241
|
* Remove file (organized file operation)
|
|
4793
5242
|
*
|
|
4794
|
-
* @param {
|
|
4795
|
-
* @param {string} args.path - File path to remove
|
|
5243
|
+
* @param {Record<string, any>} args - Arguments
|
|
4796
5244
|
* @returns {Promise<Object>} Remove result with file info
|
|
4797
5245
|
*/
|
|
4798
5246
|
async remove(args) {
|
|
@@ -4802,10 +5250,8 @@ export async function createJJ(options) {
|
|
|
4802
5250
|
/**
|
|
4803
5251
|
* Show which revision modified each line (matches `jj file annotate` / git blame)
|
|
4804
5252
|
*
|
|
4805
|
-
* @param {
|
|
4806
|
-
* @
|
|
4807
|
-
* @param {string} [args.changeId] - Change ID (defaults to working copy)
|
|
4808
|
-
* @returns {Promise<Array>} Array of line annotations with changeId, author, timestamp, and content
|
|
5253
|
+
* @param {Record<string, any>} args - Arguments
|
|
5254
|
+
* @returns {Promise<Array<any>>} Array of line annotations with changeId, author, timestamp, and content
|
|
4809
5255
|
*/
|
|
4810
5256
|
async annotate(args) {
|
|
4811
5257
|
if (!args || !args.path) {
|
|
@@ -4847,7 +5293,8 @@ export async function createJJ(options) {
|
|
|
4847
5293
|
}
|
|
4848
5294
|
|
|
4849
5295
|
// Move to parent (simplified - takes first parent)
|
|
4850
|
-
currentChangeId =
|
|
5296
|
+
currentChangeId =
|
|
5297
|
+
change.parents && change.parents.length > 0 ? change.parents[0] : null;
|
|
4851
5298
|
}
|
|
4852
5299
|
|
|
4853
5300
|
annotations.push({
|
|
@@ -4866,9 +5313,7 @@ export async function createJJ(options) {
|
|
|
4866
5313
|
* Change file permissions (matches `jj file chmod`)
|
|
4867
5314
|
* Note: Only works in Node.js, not in browsers
|
|
4868
5315
|
*
|
|
4869
|
-
* @param {
|
|
4870
|
-
* @param {string} args.path - File path
|
|
4871
|
-
* @param {number|string} args.mode - File mode (e.g., 0o755 or '755')
|
|
5316
|
+
* @param {Record<string, any>} args - Arguments
|
|
4872
5317
|
* @returns {Promise<Object>} chmod result
|
|
4873
5318
|
*/
|
|
4874
5319
|
async chmod(args) {
|
|
@@ -4886,9 +5331,13 @@ export async function createJJ(options) {
|
|
|
4886
5331
|
|
|
4887
5332
|
// Check if we're in a browser environment
|
|
4888
5333
|
if (typeof process === 'undefined' || !fs.promises.chmod) {
|
|
4889
|
-
throw new JJError(
|
|
4890
|
-
|
|
4891
|
-
|
|
5334
|
+
throw new JJError(
|
|
5335
|
+
'UNSUPPORTED_OPERATION',
|
|
5336
|
+
'chmod is not supported in browser environments',
|
|
5337
|
+
{
|
|
5338
|
+
suggestion: 'chmod only works in Node.js',
|
|
5339
|
+
}
|
|
5340
|
+
);
|
|
4892
5341
|
}
|
|
4893
5342
|
|
|
4894
5343
|
const fullPath = path.join(dir, args.path);
|
|
@@ -4914,11 +5363,10 @@ export async function createJJ(options) {
|
|
|
4914
5363
|
* This is a stub method that throws an error explaining why explicit
|
|
4915
5364
|
* file tracking is not needed in JavaScript environments.
|
|
4916
5365
|
*
|
|
4917
|
-
* @param {
|
|
4918
|
-
* @param {string} args.path - File path to track
|
|
5366
|
+
* @param {Record<string, any>} _args - Arguments
|
|
4919
5367
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
4920
5368
|
*/
|
|
4921
|
-
async track(
|
|
5369
|
+
async track(_args) {
|
|
4922
5370
|
throw new JJError(
|
|
4923
5371
|
'UNSUPPORTED_OPERATION',
|
|
4924
5372
|
'Explicit file tracking (jj file track) is not needed in JavaScript environments',
|
|
@@ -4945,11 +5393,10 @@ export async function createJJ(options) {
|
|
|
4945
5393
|
* This is a stub method that throws an error explaining why explicit
|
|
4946
5394
|
* file untracking is not needed in JavaScript environments.
|
|
4947
5395
|
*
|
|
4948
|
-
* @param {
|
|
4949
|
-
* @param {string} args.path - File path to untrack
|
|
5396
|
+
* @param {Record<string, any>} _args - Arguments
|
|
4950
5397
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
4951
5398
|
*/
|
|
4952
|
-
async untrack(
|
|
5399
|
+
async untrack(_args) {
|
|
4953
5400
|
throw new JJError(
|
|
4954
5401
|
'UNSUPPORTED_OPERATION',
|
|
4955
5402
|
'Explicit file untracking (jj file untrack) is not needed in JavaScript environments',
|
|
@@ -4979,10 +5426,7 @@ export async function createJJ(options) {
|
|
|
4979
5426
|
/**
|
|
4980
5427
|
* Add a new workspace
|
|
4981
5428
|
*
|
|
4982
|
-
* @param {
|
|
4983
|
-
* @param {string} args.path - Path for the new workspace
|
|
4984
|
-
* @param {string} [args.name] - Optional name
|
|
4985
|
-
* @param {string} [args.changeId] - Change to check out
|
|
5429
|
+
* @param {Record<string, any>} args - Workspace arguments
|
|
4986
5430
|
*/
|
|
4987
5431
|
async add(args) {
|
|
4988
5432
|
await workspaces.load();
|
|
@@ -4993,7 +5437,7 @@ export async function createJJ(options) {
|
|
|
4993
5437
|
args = { ...args, changeId: currentChangeId };
|
|
4994
5438
|
}
|
|
4995
5439
|
|
|
4996
|
-
const workspace = await workspaces.add(args);
|
|
5440
|
+
const workspace = /** @type {any} */ (await workspaces.add(/** @type {any} */ (args)));
|
|
4997
5441
|
|
|
4998
5442
|
// If a change was specified, check it out in the new workspace
|
|
4999
5443
|
if (args.changeId) {
|
|
@@ -5050,9 +5494,7 @@ export async function createJJ(options) {
|
|
|
5050
5494
|
/**
|
|
5051
5495
|
* Remove a workspace
|
|
5052
5496
|
*
|
|
5053
|
-
* @param {
|
|
5054
|
-
* @param {string} args.id - Workspace ID
|
|
5055
|
-
* @param {boolean} [args.force=false] - Force removal
|
|
5497
|
+
* @param {Record<string, any>} args - Remove arguments
|
|
5056
5498
|
*/
|
|
5057
5499
|
async remove(args) {
|
|
5058
5500
|
if (!args || !args.id) {
|
|
@@ -5082,8 +5524,7 @@ export async function createJJ(options) {
|
|
|
5082
5524
|
/**
|
|
5083
5525
|
* Forget a workspace without removing files (matches `jj workspace forget`)
|
|
5084
5526
|
*
|
|
5085
|
-
* @param {
|
|
5086
|
-
* @param {string} args.id - Workspace ID
|
|
5527
|
+
* @param {Record<string, any>} args - Forget arguments
|
|
5087
5528
|
*/
|
|
5088
5529
|
async forget(args) {
|
|
5089
5530
|
if (!args || !args.id) {
|
|
@@ -5115,7 +5556,7 @@ export async function createJJ(options) {
|
|
|
5115
5556
|
*/
|
|
5116
5557
|
async list() {
|
|
5117
5558
|
await workspaces.load();
|
|
5118
|
-
return workspaces.list();
|
|
5559
|
+
return /** @type {any[]} */ (workspaces.list());
|
|
5119
5560
|
},
|
|
5120
5561
|
|
|
5121
5562
|
/**
|
|
@@ -5125,7 +5566,7 @@ export async function createJJ(options) {
|
|
|
5125
5566
|
*/
|
|
5126
5567
|
async get(id) {
|
|
5127
5568
|
await workspaces.load();
|
|
5128
|
-
const workspace = workspaces.get(id);
|
|
5569
|
+
const workspace = /** @type {any} */ (workspaces.get(id));
|
|
5129
5570
|
if (!workspace) {
|
|
5130
5571
|
throw new JJError('WORKSPACE_NOT_FOUND', `Workspace ${id} not found`);
|
|
5131
5572
|
}
|
|
@@ -5135,9 +5576,7 @@ export async function createJJ(options) {
|
|
|
5135
5576
|
/**
|
|
5136
5577
|
* Rename a workspace (matches `jj workspace rename`)
|
|
5137
5578
|
*
|
|
5138
|
-
* @param {
|
|
5139
|
-
* @param {string} args.workspace - Current workspace name/id
|
|
5140
|
-
* @param {string} args.newName - New name for the workspace
|
|
5579
|
+
* @param {Record<string, any>} args - Rename arguments
|
|
5141
5580
|
* @returns {Promise<Object>} Updated workspace
|
|
5142
5581
|
*/
|
|
5143
5582
|
async rename(args) {
|
|
@@ -5146,7 +5585,7 @@ export async function createJJ(options) {
|
|
|
5146
5585
|
}
|
|
5147
5586
|
|
|
5148
5587
|
await workspaces.load();
|
|
5149
|
-
const workspace = workspaces.get(args.workspace);
|
|
5588
|
+
const workspace = /** @type {any} */ (workspaces.get(args.workspace));
|
|
5150
5589
|
if (!workspace) {
|
|
5151
5590
|
throw new JJError('WORKSPACE_NOT_FOUND', `Workspace ${args.workspace} not found`);
|
|
5152
5591
|
}
|
|
@@ -5174,8 +5613,7 @@ export async function createJJ(options) {
|
|
|
5174
5613
|
/**
|
|
5175
5614
|
* Get workspace root directory (matches `jj workspace root`)
|
|
5176
5615
|
*
|
|
5177
|
-
* @param {
|
|
5178
|
-
* @param {string} [args.workspace] - Workspace name/id (defaults to current)
|
|
5616
|
+
* @param {Record<string, any>} [args={}] - Arguments
|
|
5179
5617
|
* @returns {Promise<string>} Root directory path
|
|
5180
5618
|
*/
|
|
5181
5619
|
async root(args = {}) {
|
|
@@ -5187,10 +5625,10 @@ export async function createJJ(options) {
|
|
|
5187
5625
|
await workspaces.load();
|
|
5188
5626
|
|
|
5189
5627
|
// Try to get by ID first, then by name
|
|
5190
|
-
let workspace = workspaces.get(args.workspace);
|
|
5628
|
+
let workspace = /** @type {any} */ (workspaces.get(args.workspace));
|
|
5191
5629
|
if (!workspace) {
|
|
5192
5630
|
// Search by name
|
|
5193
|
-
const allWorkspaces = workspaces.list();
|
|
5631
|
+
const allWorkspaces = /** @type {any[]} */ (workspaces.list());
|
|
5194
5632
|
workspace = allWorkspaces.find((ws) => ws.name === args.workspace);
|
|
5195
5633
|
}
|
|
5196
5634
|
|
|
@@ -5204,15 +5642,14 @@ export async function createJJ(options) {
|
|
|
5204
5642
|
/**
|
|
5205
5643
|
* Update stale workspaces (matches `jj workspace update-stale`)
|
|
5206
5644
|
*
|
|
5207
|
-
* @param {
|
|
5208
|
-
* @param {string} [args.workspace] - Specific workspace to update (defaults to all stale)
|
|
5645
|
+
* @param {Record<string, any>} [args={}] - Arguments
|
|
5209
5646
|
* @returns {Promise<Object>} Update result
|
|
5210
5647
|
*/
|
|
5211
5648
|
async updateStale(args = {}) {
|
|
5212
5649
|
await workspaces.load();
|
|
5213
5650
|
await graph.load();
|
|
5214
5651
|
|
|
5215
|
-
const allWorkspaces = workspaces.list();
|
|
5652
|
+
const allWorkspaces = /** @type {any[]} */ (workspaces.list());
|
|
5216
5653
|
const staleWorkspaces = [];
|
|
5217
5654
|
|
|
5218
5655
|
for (const ws of allWorkspaces) {
|
|
@@ -5235,9 +5672,9 @@ export async function createJJ(options) {
|
|
|
5235
5672
|
|
|
5236
5673
|
if (args.workspace && toUpdate.length === 0) {
|
|
5237
5674
|
// Look up the workspace to get its name for a better error message
|
|
5238
|
-
let workspace = workspaces.get(args.workspace);
|
|
5675
|
+
let workspace = /** @type {any} */ (workspaces.get(args.workspace));
|
|
5239
5676
|
if (!workspace) {
|
|
5240
|
-
const allWorkspaces = workspaces.list();
|
|
5677
|
+
const allWorkspaces = /** @type {any[]} */ (workspaces.list());
|
|
5241
5678
|
workspace = allWorkspaces.find((ws) => ws.name === args.workspace);
|
|
5242
5679
|
}
|
|
5243
5680
|
const workspaceName = workspace ? workspace.name : args.workspace;
|
|
@@ -5305,7 +5742,7 @@ export async function createJJ(options) {
|
|
|
5305
5742
|
* Queue a background operation
|
|
5306
5743
|
*
|
|
5307
5744
|
* @param {Function} operation - Async operation
|
|
5308
|
-
* @param {
|
|
5745
|
+
* @param {Record<string, any>} [opts] - Options
|
|
5309
5746
|
*/
|
|
5310
5747
|
async queue(operation, opts) {
|
|
5311
5748
|
if (!jj.backgroundOps) {
|
|
@@ -5319,7 +5756,7 @@ export async function createJJ(options) {
|
|
|
5319
5756
|
/**
|
|
5320
5757
|
* List background operations
|
|
5321
5758
|
*
|
|
5322
|
-
* @param {
|
|
5759
|
+
* @param {Record<string, any>} [opts] - Filter options
|
|
5323
5760
|
*/
|
|
5324
5761
|
listOperations(opts) {
|
|
5325
5762
|
if (!jj.backgroundOps) {
|
|
@@ -5331,7 +5768,7 @@ export async function createJJ(options) {
|
|
|
5331
5768
|
/**
|
|
5332
5769
|
* Enable auto-snapshot on file changes
|
|
5333
5770
|
*
|
|
5334
|
-
* @param {
|
|
5771
|
+
* @param {Record<string, any>} [opts] - Options
|
|
5335
5772
|
*/
|
|
5336
5773
|
async enableAutoSnapshot(opts) {
|
|
5337
5774
|
if (!jj.backgroundOps) {
|
|
@@ -5373,7 +5810,7 @@ export async function createJJ(options) {
|
|
|
5373
5810
|
/**
|
|
5374
5811
|
* List all bookmarks
|
|
5375
5812
|
*
|
|
5376
|
-
* @returns {Promise<Array
|
|
5813
|
+
* @returns {Promise<Array<any>>} Array of bookmarks with name and changeId
|
|
5377
5814
|
*/
|
|
5378
5815
|
async list() {
|
|
5379
5816
|
await bookmarks.load();
|
|
@@ -5383,9 +5820,7 @@ export async function createJJ(options) {
|
|
|
5383
5820
|
/**
|
|
5384
5821
|
* Set/create a bookmark (matches `jj bookmark set`)
|
|
5385
5822
|
*
|
|
5386
|
-
* @param {
|
|
5387
|
-
* @param {string} args.name - Bookmark name
|
|
5388
|
-
* @param {string} args.changeId - Change ID to point to
|
|
5823
|
+
* @param {Record<string, any>} args - Bookmark arguments
|
|
5389
5824
|
* @returns {Promise<Object>} Bookmark info
|
|
5390
5825
|
*/
|
|
5391
5826
|
async set(args) {
|
|
@@ -5403,7 +5838,8 @@ export async function createJJ(options) {
|
|
|
5403
5838
|
if (!args || !args.name || !args.changeId) {
|
|
5404
5839
|
throw new JJError('INVALID_ARGUMENT', 'Missing name or changeId', {
|
|
5405
5840
|
args,
|
|
5406
|
-
suggestion:
|
|
5841
|
+
suggestion:
|
|
5842
|
+
'Provide both: { name: "main", changeId: "abc123..." } (or use target/change/revision instead of changeId)',
|
|
5407
5843
|
});
|
|
5408
5844
|
}
|
|
5409
5845
|
|
|
@@ -5433,9 +5869,7 @@ export async function createJJ(options) {
|
|
|
5433
5869
|
*
|
|
5434
5870
|
* Unlike `set()`, this will fail if the bookmark already exists.
|
|
5435
5871
|
*
|
|
5436
|
-
* @param {
|
|
5437
|
-
* @param {string} args.name - Bookmark name
|
|
5438
|
-
* @param {string} [args.changeId] - Change ID to point to (default: working copy)
|
|
5872
|
+
* @param {Record<string, any>} args - Bookmark arguments
|
|
5439
5873
|
* @returns {Promise<Object>} Bookmark info
|
|
5440
5874
|
*/
|
|
5441
5875
|
async create(args) {
|
|
@@ -5452,7 +5886,8 @@ export async function createJJ(options) {
|
|
|
5452
5886
|
|
|
5453
5887
|
if (!args || !args.name) {
|
|
5454
5888
|
throw new JJError('INVALID_ARGUMENT', 'Missing name argument', {
|
|
5455
|
-
suggestion:
|
|
5889
|
+
suggestion:
|
|
5890
|
+
'Provide: { name: "main", changeId: "abc123..." } or { name: "main" } for working copy',
|
|
5456
5891
|
});
|
|
5457
5892
|
}
|
|
5458
5893
|
|
|
@@ -5469,7 +5904,8 @@ export async function createJJ(options) {
|
|
|
5469
5904
|
const existingBookmark = await bookmarks.get(args.name);
|
|
5470
5905
|
if (existingBookmark) {
|
|
5471
5906
|
throw new JJError('BOOKMARK_EXISTS', `Bookmark ${args.name} already exists`, {
|
|
5472
|
-
suggestion:
|
|
5907
|
+
suggestion:
|
|
5908
|
+
'Use bookmark.set() to update an existing bookmark, or bookmark.move() to move it',
|
|
5473
5909
|
});
|
|
5474
5910
|
}
|
|
5475
5911
|
|
|
@@ -5496,9 +5932,7 @@ export async function createJJ(options) {
|
|
|
5496
5932
|
/**
|
|
5497
5933
|
* Move a bookmark to a different change (matches `jj bookmark move`)
|
|
5498
5934
|
*
|
|
5499
|
-
* @param {
|
|
5500
|
-
* @param {string} args.name - Bookmark name
|
|
5501
|
-
* @param {string} args.to - Target change ID
|
|
5935
|
+
* @param {Record<string, any>} args - Bookmark arguments
|
|
5502
5936
|
* @returns {Promise<Object>} Move result
|
|
5503
5937
|
*/
|
|
5504
5938
|
async move(args) {
|
|
@@ -5518,7 +5952,8 @@ export async function createJJ(options) {
|
|
|
5518
5952
|
if (!args || !args.name || !args.to) {
|
|
5519
5953
|
throw new JJError('INVALID_ARGUMENT', 'Missing name or to', {
|
|
5520
5954
|
args,
|
|
5521
|
-
suggestion:
|
|
5955
|
+
suggestion:
|
|
5956
|
+
'Provide both: { name: "main", to: "abc123..." } (or use target/changeId/change/revision instead of to)',
|
|
5522
5957
|
});
|
|
5523
5958
|
}
|
|
5524
5959
|
|
|
@@ -5547,11 +5982,68 @@ export async function createJJ(options) {
|
|
|
5547
5982
|
return { name: args.name, from: bookmarkChangeId, to: args.to };
|
|
5548
5983
|
},
|
|
5549
5984
|
|
|
5985
|
+
/**
|
|
5986
|
+
* Advance a bookmark to a descendant change (matches `jj bookmark advance`,
|
|
5987
|
+
* jj v0.39). Unlike move(), advance() only moves the bookmark *forward*
|
|
5988
|
+
* along the graph — the target must be a descendant of the bookmark's
|
|
5989
|
+
* current position (defaults to the working copy).
|
|
5990
|
+
*
|
|
5991
|
+
* @param {Record<string, any>} args - Arguments
|
|
5992
|
+
* @returns {Promise<Object>} { name, from, to }
|
|
5993
|
+
*/
|
|
5994
|
+
async advance(args) {
|
|
5995
|
+
if (args) {
|
|
5996
|
+
const alias = args.to || args.target || args.changeId || args.change || args.revision;
|
|
5997
|
+
if (alias) args = { ...args, to: alias };
|
|
5998
|
+
}
|
|
5999
|
+
if (!args || !args.name) {
|
|
6000
|
+
throw new JJError('INVALID_ARGUMENT', 'Missing bookmark name', {
|
|
6001
|
+
suggestion: 'Provide the bookmark name: { name: "main", to: "abc123..." }',
|
|
6002
|
+
});
|
|
6003
|
+
}
|
|
6004
|
+
|
|
6005
|
+
await bookmarks.load();
|
|
6006
|
+
await graph.load();
|
|
6007
|
+
|
|
6008
|
+
const from = await bookmarks.get(args.name);
|
|
6009
|
+
if (!from) {
|
|
6010
|
+
throw new JJError('NOT_FOUND', `Bookmark ${args.name} not found`);
|
|
6011
|
+
}
|
|
6012
|
+
|
|
6013
|
+
const to = args.to || workingCopy.getCurrentChangeId();
|
|
6014
|
+
|
|
6015
|
+
// The target must be a descendant of (or equal to) the current position.
|
|
6016
|
+
const descendants = new Set(await revset.getDescendants(from));
|
|
6017
|
+
if (to !== from && !descendants.has(to)) {
|
|
6018
|
+
throw new JJError(
|
|
6019
|
+
'BOOKMARK_NOT_ADVANCEABLE',
|
|
6020
|
+
`Cannot advance bookmark ${args.name}: ${to.slice(0, 8)} is not a descendant of ${from.slice(0, 8)}`,
|
|
6021
|
+
{ suggestion: 'Use bookmark.move() to move a bookmark backward or sideways' }
|
|
6022
|
+
);
|
|
6023
|
+
}
|
|
6024
|
+
|
|
6025
|
+
await bookmarks.move(args.name, to);
|
|
6026
|
+
|
|
6027
|
+
await oplog.recordOperation({
|
|
6028
|
+
timestamp: new Date().toISOString(),
|
|
6029
|
+
user: await getUserOplogInfo(),
|
|
6030
|
+
description: `bookmark advance ${args.name}`,
|
|
6031
|
+
parents: [],
|
|
6032
|
+
view: {
|
|
6033
|
+
bookmarks: { [args.name]: to },
|
|
6034
|
+
remoteBookmarks: {},
|
|
6035
|
+
heads: [],
|
|
6036
|
+
workingCopy: workingCopy.getCurrentChangeId(),
|
|
6037
|
+
},
|
|
6038
|
+
});
|
|
6039
|
+
|
|
6040
|
+
return { name: args.name, from, to };
|
|
6041
|
+
},
|
|
6042
|
+
|
|
5550
6043
|
/**
|
|
5551
6044
|
* Delete a bookmark (matches `jj bookmark delete`)
|
|
5552
6045
|
*
|
|
5553
|
-
* @param {
|
|
5554
|
-
* @param {string} args.name - Bookmark name to delete
|
|
6046
|
+
* @param {Record<string, any>} args - Bookmark arguments
|
|
5555
6047
|
* @returns {Promise<Object>} Deletion result
|
|
5556
6048
|
*/
|
|
5557
6049
|
async delete(args) {
|
|
@@ -5562,7 +6054,7 @@ export async function createJJ(options) {
|
|
|
5562
6054
|
}
|
|
5563
6055
|
|
|
5564
6056
|
await bookmarks.load();
|
|
5565
|
-
const bookmark = bookmarks.get(args.name);
|
|
6057
|
+
const bookmark = await bookmarks.get(args.name);
|
|
5566
6058
|
if (!bookmark) {
|
|
5567
6059
|
throw new JJError('NOT_FOUND', `Bookmark ${args.name} not found`);
|
|
5568
6060
|
}
|
|
@@ -5590,9 +6082,7 @@ export async function createJJ(options) {
|
|
|
5590
6082
|
/**
|
|
5591
6083
|
* Rename a bookmark (matches `jj bookmark rename`)
|
|
5592
6084
|
*
|
|
5593
|
-
* @param {
|
|
5594
|
-
* @param {string} args.oldName - Current bookmark name
|
|
5595
|
-
* @param {string} args.newName - New bookmark name
|
|
6085
|
+
* @param {Record<string, any>} args - Bookmark arguments
|
|
5596
6086
|
* @returns {Promise<Object>} Rename result
|
|
5597
6087
|
*/
|
|
5598
6088
|
async rename(args) {
|
|
@@ -5633,9 +6123,7 @@ export async function createJJ(options) {
|
|
|
5633
6123
|
/**
|
|
5634
6124
|
* Track a remote bookmark (matches `jj bookmark track`)
|
|
5635
6125
|
*
|
|
5636
|
-
* @param {
|
|
5637
|
-
* @param {string} args.name - Bookmark name to track
|
|
5638
|
-
* @param {string} [args.remote='origin'] - Remote name
|
|
6126
|
+
* @param {Record<string, any>} args - Bookmark arguments
|
|
5639
6127
|
* @returns {Promise<Object>} Track result
|
|
5640
6128
|
*/
|
|
5641
6129
|
async track(args) {
|
|
@@ -5650,6 +6138,7 @@ export async function createJJ(options) {
|
|
|
5650
6138
|
|
|
5651
6139
|
// Store tracking info in bookmarks metadata
|
|
5652
6140
|
await bookmarks.load();
|
|
6141
|
+
/** @type {Record<string, any>} */
|
|
5653
6142
|
const tracking = bookmarks.tracking || {};
|
|
5654
6143
|
tracking[args.name] = { remote, remoteName: args.name };
|
|
5655
6144
|
bookmarks.tracking = tracking;
|
|
@@ -5675,8 +6164,7 @@ export async function createJJ(options) {
|
|
|
5675
6164
|
/**
|
|
5676
6165
|
* Untrack a remote bookmark (matches `jj bookmark untrack`)
|
|
5677
6166
|
*
|
|
5678
|
-
* @param {
|
|
5679
|
-
* @param {string} args.name - Bookmark name to untrack
|
|
6167
|
+
* @param {Record<string, any>} args - Bookmark arguments
|
|
5680
6168
|
* @returns {Promise<Object>} Untrack result
|
|
5681
6169
|
*/
|
|
5682
6170
|
async untrack(args) {
|
|
@@ -5688,6 +6176,7 @@ export async function createJJ(options) {
|
|
|
5688
6176
|
|
|
5689
6177
|
// Remove tracking info
|
|
5690
6178
|
await bookmarks.load();
|
|
6179
|
+
/** @type {Record<string, any>} */
|
|
5691
6180
|
const tracking = bookmarks.tracking || {};
|
|
5692
6181
|
const wasTracking = tracking[args.name];
|
|
5693
6182
|
delete tracking[args.name];
|
|
@@ -5714,9 +6203,7 @@ export async function createJJ(options) {
|
|
|
5714
6203
|
/**
|
|
5715
6204
|
* Forget a remote bookmark (matches `jj bookmark forget`)
|
|
5716
6205
|
*
|
|
5717
|
-
* @param {
|
|
5718
|
-
* @param {string} args.name - Bookmark name to forget
|
|
5719
|
-
* @param {string} [args.remote='origin'] - Remote name
|
|
6206
|
+
* @param {Record<string, any>} args - Bookmark arguments
|
|
5720
6207
|
* @returns {Promise<Object>} Forget result
|
|
5721
6208
|
*/
|
|
5722
6209
|
async forget(args) {
|
|
@@ -5730,6 +6217,7 @@ export async function createJJ(options) {
|
|
|
5730
6217
|
|
|
5731
6218
|
// Untrack and remove local reference
|
|
5732
6219
|
await bookmarks.load();
|
|
6220
|
+
/** @type {Record<string, any>} */
|
|
5733
6221
|
const tracking = bookmarks.tracking || {};
|
|
5734
6222
|
delete tracking[args.name];
|
|
5735
6223
|
bookmarks.tracking = tracking;
|
|
@@ -5776,16 +6264,15 @@ export async function createJJ(options) {
|
|
|
5776
6264
|
* Tags are immutable references that cannot be moved once created.
|
|
5777
6265
|
* Unlike bookmarks, tags are permanent markers typically used for releases.
|
|
5778
6266
|
*
|
|
5779
|
-
* @param {
|
|
5780
|
-
* @param {string} args.name - Tag name
|
|
5781
|
-
* @param {string} [args.changeId] - Change ID to tag (default: working copy)
|
|
6267
|
+
* @param {Record<string, any>} args - Tag arguments
|
|
5782
6268
|
* @returns {Promise<{name: string, changeId: string}>} Created tag
|
|
5783
6269
|
* @throws {JJError} If tag already exists or name is invalid
|
|
5784
6270
|
*/
|
|
5785
6271
|
async create(args) {
|
|
5786
6272
|
if (!args || args.name === undefined || args.name === null) {
|
|
5787
6273
|
throw new JJError('INVALID_ARGUMENT', 'Missing name argument', {
|
|
5788
|
-
suggestion:
|
|
6274
|
+
suggestion:
|
|
6275
|
+
'Provide: { name: "v1.0.0", changeId: "abc123..." } or { name: "v1.0.0" } for working copy',
|
|
5789
6276
|
});
|
|
5790
6277
|
}
|
|
5791
6278
|
|
|
@@ -5808,6 +6295,10 @@ export async function createJJ(options) {
|
|
|
5808
6295
|
view: {
|
|
5809
6296
|
bookmarks: {},
|
|
5810
6297
|
remoteBookmarks: {},
|
|
6298
|
+
// Tag delta for this operation (mirrors how `bookmarks` records
|
|
6299
|
+
// its delta above), so operations.revert() can diff it — see
|
|
6300
|
+
// issue #12.
|
|
6301
|
+
tags: { [args.name]: changeId },
|
|
5811
6302
|
heads: [],
|
|
5812
6303
|
workingCopy: workingCopy.getCurrentChangeId(),
|
|
5813
6304
|
},
|
|
@@ -5817,11 +6308,57 @@ export async function createJJ(options) {
|
|
|
5817
6308
|
},
|
|
5818
6309
|
|
|
5819
6310
|
/**
|
|
5820
|
-
*
|
|
6311
|
+
* Create or update (move) a tag (matches `jj tag set`, jj v0.35).
|
|
6312
|
+
*
|
|
6313
|
+
* Unlike create(), set() is an upsert: it will point an existing tag at a
|
|
6314
|
+
* new change instead of erroring.
|
|
6315
|
+
*
|
|
6316
|
+
* @param {Record<string, any>} args - Tag arguments
|
|
6317
|
+
* @returns {Promise<{name: string, changeId: string, updated: boolean}>}
|
|
6318
|
+
*/
|
|
6319
|
+
async set(args) {
|
|
6320
|
+
if (!args || args.name === undefined || args.name === null) {
|
|
6321
|
+
throw new JJError('INVALID_ARGUMENT', 'Missing name argument', {
|
|
6322
|
+
suggestion: 'Provide: { name: "v1.0.0", changeId: "abc123..." }',
|
|
6323
|
+
});
|
|
6324
|
+
}
|
|
6325
|
+
|
|
6326
|
+
let changeId = args.changeId || args.change || args.revision;
|
|
6327
|
+
if (!changeId) {
|
|
6328
|
+
await workingCopy.load();
|
|
6329
|
+
changeId = workingCopy.getCurrentChangeId();
|
|
6330
|
+
}
|
|
6331
|
+
|
|
6332
|
+
await tags.load();
|
|
6333
|
+
const existed = await tags.exists(args.name);
|
|
6334
|
+
if (existed) {
|
|
6335
|
+
await tags.delete(args.name);
|
|
6336
|
+
}
|
|
6337
|
+
const result = await tags.create(args.name, changeId);
|
|
6338
|
+
|
|
6339
|
+
await oplog.recordOperation({
|
|
6340
|
+
timestamp: new Date().toISOString(),
|
|
6341
|
+
user: await getUserOplogInfo(),
|
|
6342
|
+
description: `tag set ${args.name}`,
|
|
6343
|
+
parents: [],
|
|
6344
|
+
view: {
|
|
6345
|
+
bookmarks: {},
|
|
6346
|
+
remoteBookmarks: {},
|
|
6347
|
+
tags: { [args.name]: changeId },
|
|
6348
|
+
heads: [],
|
|
6349
|
+
workingCopy: workingCopy.getCurrentChangeId(),
|
|
6350
|
+
},
|
|
6351
|
+
});
|
|
6352
|
+
|
|
6353
|
+
return { ...result, updated: existed };
|
|
6354
|
+
},
|
|
6355
|
+
|
|
6356
|
+
/**
|
|
6357
|
+
* List all tags. Each entry also carries a `tracking: { remote, ref }`
|
|
6358
|
+
* field when the tag is tracking a remote (jj v0.44 `jj tag track`).
|
|
5821
6359
|
*
|
|
5822
|
-
* @param {
|
|
5823
|
-
* @
|
|
5824
|
-
* @returns {Promise<Array<{name: string, changeId: string}>>} List of tags
|
|
6360
|
+
* @param {Record<string, any>} [args] - Optional filter arguments
|
|
6361
|
+
* @returns {Promise<Array<Record<string, any>>>} List of tags
|
|
5825
6362
|
*/
|
|
5826
6363
|
async list(args) {
|
|
5827
6364
|
const pattern = args?.pattern;
|
|
@@ -5831,8 +6368,7 @@ export async function createJJ(options) {
|
|
|
5831
6368
|
/**
|
|
5832
6369
|
* Delete a tag
|
|
5833
6370
|
*
|
|
5834
|
-
* @param {
|
|
5835
|
-
* @param {string} args.name - Tag name to delete
|
|
6371
|
+
* @param {Record<string, any>} args - Tag arguments
|
|
5836
6372
|
* @returns {Promise<{deleted: string}>} Deletion result
|
|
5837
6373
|
* @throws {JJError} If tag doesn't exist
|
|
5838
6374
|
*/
|
|
@@ -5854,6 +6390,7 @@ export async function createJJ(options) {
|
|
|
5854
6390
|
view: {
|
|
5855
6391
|
bookmarks: {},
|
|
5856
6392
|
remoteBookmarks: {},
|
|
6393
|
+
tags: {},
|
|
5857
6394
|
heads: [],
|
|
5858
6395
|
workingCopy: workingCopy.getCurrentChangeId(),
|
|
5859
6396
|
},
|
|
@@ -5861,6 +6398,94 @@ export async function createJJ(options) {
|
|
|
5861
6398
|
|
|
5862
6399
|
return { deleted: args.name };
|
|
5863
6400
|
},
|
|
6401
|
+
|
|
6402
|
+
/**
|
|
6403
|
+
* Associate a local tag with a remote tag so it is updated on fetch
|
|
6404
|
+
* (matches `jj tag track`, new in jj v0.44).
|
|
6405
|
+
*
|
|
6406
|
+
* jj v0.44 made tags remote-trackable just like bookmarks: fetched
|
|
6407
|
+
* tags are tracked by default, and `jj tag track`/`untrack` let you
|
|
6408
|
+
* opt individual tags in or out by hand. isomorphic-jj doesn't (yet)
|
|
6409
|
+
* auto-import remote tags during `git.fetch()` — see CHANGELOG — so
|
|
6410
|
+
* this records tracking intent/state the same way `bookmark.track()`
|
|
6411
|
+
* does, without changing fetch's own behavior.
|
|
6412
|
+
*
|
|
6413
|
+
* @param {Record<string, any>} args - Tag arguments: `{ name, remote }`
|
|
6414
|
+
* (`name` is the local tag name to track; `remote` defaults to `'origin'`)
|
|
6415
|
+
* @returns {Promise<{name: string, remote: string, tracking: boolean}>}
|
|
6416
|
+
*/
|
|
6417
|
+
async track(args) {
|
|
6418
|
+
if (!args || !args.name) {
|
|
6419
|
+
throw new JJError('INVALID_ARGUMENT', 'Missing tag name', {
|
|
6420
|
+
suggestion: 'Provide tag name: { name: "v1.0.0", remote: "origin" }',
|
|
6421
|
+
});
|
|
6422
|
+
}
|
|
6423
|
+
|
|
6424
|
+
const remote = args.remote || 'origin';
|
|
6425
|
+
const remoteTagName = `${remote}/${args.name}`;
|
|
6426
|
+
|
|
6427
|
+
const tagMap = await tags.load();
|
|
6428
|
+
/** @type {Record<string, any>} */
|
|
6429
|
+
const tracking = tags.tracking || {};
|
|
6430
|
+
tracking[args.name] = { remote, remoteName: args.name };
|
|
6431
|
+
tags.tracking = tracking;
|
|
6432
|
+
await tags.save(tagMap);
|
|
6433
|
+
|
|
6434
|
+
// Record operation
|
|
6435
|
+
await oplog.recordOperation({
|
|
6436
|
+
timestamp: new Date().toISOString(),
|
|
6437
|
+
user: await getUserOplogInfo(),
|
|
6438
|
+
description: `tag track ${args.name} from ${remote}`,
|
|
6439
|
+
parents: [],
|
|
6440
|
+
view: {
|
|
6441
|
+
bookmarks: {},
|
|
6442
|
+
remoteBookmarks: { [remoteTagName]: true },
|
|
6443
|
+
heads: [],
|
|
6444
|
+
workingCopy: workingCopy.getCurrentChangeId(),
|
|
6445
|
+
},
|
|
6446
|
+
});
|
|
6447
|
+
|
|
6448
|
+
return { name: args.name, remote, tracking: true };
|
|
6449
|
+
},
|
|
6450
|
+
|
|
6451
|
+
/**
|
|
6452
|
+
* Stop tracking a remote tag (matches `jj tag untrack`, new in jj v0.44).
|
|
6453
|
+
*
|
|
6454
|
+
* @param {Record<string, any>} args - Tag arguments: `{ name }`, the
|
|
6455
|
+
* local tag name to stop tracking
|
|
6456
|
+
* @returns {Promise<{name: string, tracking: boolean, wasTracking: boolean}>}
|
|
6457
|
+
*/
|
|
6458
|
+
async untrack(args) {
|
|
6459
|
+
if (!args || !args.name) {
|
|
6460
|
+
throw new JJError('INVALID_ARGUMENT', 'Missing tag name', {
|
|
6461
|
+
suggestion: 'Provide tag name: { name: "v1.0.0" }',
|
|
6462
|
+
});
|
|
6463
|
+
}
|
|
6464
|
+
|
|
6465
|
+
const tagMap = await tags.load();
|
|
6466
|
+
/** @type {Record<string, any>} */
|
|
6467
|
+
const tracking = tags.tracking || {};
|
|
6468
|
+
const wasTracking = tracking[args.name];
|
|
6469
|
+
delete tracking[args.name];
|
|
6470
|
+
tags.tracking = tracking;
|
|
6471
|
+
await tags.save(tagMap);
|
|
6472
|
+
|
|
6473
|
+
// Record operation
|
|
6474
|
+
await oplog.recordOperation({
|
|
6475
|
+
timestamp: new Date().toISOString(),
|
|
6476
|
+
user: await getUserOplogInfo(),
|
|
6477
|
+
description: `tag untrack ${args.name}`,
|
|
6478
|
+
parents: [],
|
|
6479
|
+
view: {
|
|
6480
|
+
bookmarks: {},
|
|
6481
|
+
remoteBookmarks: {},
|
|
6482
|
+
heads: [],
|
|
6483
|
+
workingCopy: workingCopy.getCurrentChangeId(),
|
|
6484
|
+
},
|
|
6485
|
+
});
|
|
6486
|
+
|
|
6487
|
+
return { name: args.name, tracking: false, wasTracking: !!wasTracking };
|
|
6488
|
+
},
|
|
5864
6489
|
},
|
|
5865
6490
|
|
|
5866
6491
|
/**
|
|
@@ -5870,7 +6495,7 @@ export async function createJJ(options) {
|
|
|
5870
6495
|
/**
|
|
5871
6496
|
* Fetch from remote (alias to jj.git.fetch for convenience)
|
|
5872
6497
|
*
|
|
5873
|
-
* @param {
|
|
6498
|
+
* @param {Record<string, any>} args - Fetch arguments
|
|
5874
6499
|
* @returns {Promise<Object>} Fetch result
|
|
5875
6500
|
*/
|
|
5876
6501
|
async fetch(args) {
|
|
@@ -5880,7 +6505,7 @@ export async function createJJ(options) {
|
|
|
5880
6505
|
/**
|
|
5881
6506
|
* Push to remote (alias to jj.git.push for convenience)
|
|
5882
6507
|
*
|
|
5883
|
-
* @param {
|
|
6508
|
+
* @param {Record<string, any>} args - Push arguments
|
|
5884
6509
|
* @returns {Promise<Object>} Push result
|
|
5885
6510
|
*/
|
|
5886
6511
|
async push(args) {
|
|
@@ -5890,7 +6515,7 @@ export async function createJJ(options) {
|
|
|
5890
6515
|
/**
|
|
5891
6516
|
* Add a remote (alias to jj.git.remote.add for convenience)
|
|
5892
6517
|
*
|
|
5893
|
-
* @param {
|
|
6518
|
+
* @param {Record<string, any>} args - Remote arguments
|
|
5894
6519
|
* @returns {Promise<Object>} Added remote info
|
|
5895
6520
|
*/
|
|
5896
6521
|
async add(args) {
|
|
@@ -5905,8 +6530,7 @@ export async function createJJ(options) {
|
|
|
5905
6530
|
/**
|
|
5906
6531
|
* Get a configuration value (matches `jj config get`)
|
|
5907
6532
|
*
|
|
5908
|
-
* @param {
|
|
5909
|
-
* @param {string} args.name - Config key (e.g., 'user.name')
|
|
6533
|
+
* @param {Record<string, any>} args - Config arguments
|
|
5910
6534
|
* @returns {Promise<any>} Config value
|
|
5911
6535
|
*/
|
|
5912
6536
|
async get(args) {
|
|
@@ -5931,9 +6555,7 @@ export async function createJJ(options) {
|
|
|
5931
6555
|
/**
|
|
5932
6556
|
* Set a configuration value (matches `jj config set`)
|
|
5933
6557
|
*
|
|
5934
|
-
* @param {
|
|
5935
|
-
* @param {string} args.name - Config key (e.g., 'user.name')
|
|
5936
|
-
* @param {any} args.value - Config value
|
|
6558
|
+
* @param {Record<string, any>} args - Config arguments
|
|
5937
6559
|
* @returns {Promise<Object>} Set result
|
|
5938
6560
|
*/
|
|
5939
6561
|
async set(args) {
|
|
@@ -5980,9 +6602,7 @@ export async function createJJ(options) {
|
|
|
5980
6602
|
* v0.35.0: Loads workspace-config.json if present
|
|
5981
6603
|
* v0.36.0: Accepts optional config object for programmatic configuration
|
|
5982
6604
|
*
|
|
5983
|
-
* @param {
|
|
5984
|
-
* @param {Object} [opts.workspace] - Workspace-specific config to merge (highest priority)
|
|
5985
|
-
* @param {Object} [opts.override] - Config object to merge over loaded config
|
|
6605
|
+
* @param {Record<string, any>} [opts] - Optional config options
|
|
5986
6606
|
* @returns {Promise<void>}
|
|
5987
6607
|
*
|
|
5988
6608
|
* @example
|
|
@@ -6077,8 +6697,7 @@ export async function createJJ(options) {
|
|
|
6077
6697
|
/**
|
|
6078
6698
|
* Set sparse patterns (replaces all existing patterns)
|
|
6079
6699
|
*
|
|
6080
|
-
* @param {
|
|
6081
|
-
* @param {string[]} args.patterns - Array of glob patterns
|
|
6700
|
+
* @param {Record<string, any>} args - Arguments
|
|
6082
6701
|
* @returns {Promise<{patterns: string[]}>} Result with current patterns
|
|
6083
6702
|
*/
|
|
6084
6703
|
async set(args) {
|
|
@@ -6112,8 +6731,7 @@ export async function createJJ(options) {
|
|
|
6112
6731
|
/**
|
|
6113
6732
|
* Add patterns to sparse checkout
|
|
6114
6733
|
*
|
|
6115
|
-
* @param {
|
|
6116
|
-
* @param {string[]} args.patterns - Array of glob patterns to add
|
|
6734
|
+
* @param {Record<string, any>} args - Arguments
|
|
6117
6735
|
* @returns {Promise<{patterns: string[]}>} Result with current patterns
|
|
6118
6736
|
*/
|
|
6119
6737
|
async add(args) {
|
|
@@ -6157,8 +6775,7 @@ export async function createJJ(options) {
|
|
|
6157
6775
|
/**
|
|
6158
6776
|
* Remove patterns from sparse checkout
|
|
6159
6777
|
*
|
|
6160
|
-
* @param {
|
|
6161
|
-
* @param {string[]} args.patterns - Array of glob patterns to remove
|
|
6778
|
+
* @param {Record<string, any>} args - Arguments
|
|
6162
6779
|
* @returns {Promise<{patterns: string[]}>} Result with current patterns
|
|
6163
6780
|
*/
|
|
6164
6781
|
async remove(args) {
|
|
@@ -6172,7 +6789,7 @@ export async function createJJ(options) {
|
|
|
6172
6789
|
const currentPatterns = workingCopy.getSparsePatterns();
|
|
6173
6790
|
|
|
6174
6791
|
// Remove specified patterns
|
|
6175
|
-
const updatedPatterns = currentPatterns.filter(p => !args.patterns.includes(p));
|
|
6792
|
+
const updatedPatterns = currentPatterns.filter((p) => !args.patterns.includes(p));
|
|
6176
6793
|
|
|
6177
6794
|
await workingCopy.setSparsePatterns(updatedPatterns);
|
|
6178
6795
|
await workingCopy.save();
|
|
@@ -6241,9 +6858,7 @@ export async function createJJ(options) {
|
|
|
6241
6858
|
/**
|
|
6242
6859
|
* Start bisect session
|
|
6243
6860
|
*
|
|
6244
|
-
* @param {
|
|
6245
|
-
* @param {string} args.good - Known good change ID
|
|
6246
|
-
* @param {string} args.bad - Known bad change ID
|
|
6861
|
+
* @param {Record<string, any>} args - Bisect arguments
|
|
6247
6862
|
* @returns {Promise<Object>} Bisect state
|
|
6248
6863
|
*/
|
|
6249
6864
|
async start(args) {
|
|
@@ -6256,7 +6871,7 @@ export async function createJJ(options) {
|
|
|
6256
6871
|
await graph.load();
|
|
6257
6872
|
|
|
6258
6873
|
// Check if bisect is already active
|
|
6259
|
-
const existingState = await storage.read('bisect/state.json');
|
|
6874
|
+
const existingState = /** @type {any} */ (await storage.read('bisect/state.json'));
|
|
6260
6875
|
if (existingState && existingState.active) {
|
|
6261
6876
|
throw new JJError('BISECT_ALREADY_ACTIVE', 'Bisect session already in progress', {
|
|
6262
6877
|
suggestion: 'Use bisect.reset() to end current session first',
|
|
@@ -6279,9 +6894,8 @@ export async function createJJ(options) {
|
|
|
6279
6894
|
|
|
6280
6895
|
// Pick the middle change
|
|
6281
6896
|
// If no candidates between good and bad, test bad itself
|
|
6282
|
-
const current =
|
|
6283
|
-
? candidates[Math.floor(candidates.length / 2)]
|
|
6284
|
-
: args.bad;
|
|
6897
|
+
const current =
|
|
6898
|
+
candidates.length > 0 ? candidates[Math.floor(candidates.length / 2)] : args.bad;
|
|
6285
6899
|
|
|
6286
6900
|
const bisectState = {
|
|
6287
6901
|
active: true,
|
|
@@ -6304,7 +6918,7 @@ export async function createJJ(options) {
|
|
|
6304
6918
|
* @returns {Promise<Object>} Updated bisect state
|
|
6305
6919
|
*/
|
|
6306
6920
|
async good() {
|
|
6307
|
-
const bisectState = await storage.read('bisect/state.json');
|
|
6921
|
+
const bisectState = /** @type {any} */ (await storage.read('bisect/state.json'));
|
|
6308
6922
|
|
|
6309
6923
|
if (!bisectState || !bisectState.active) {
|
|
6310
6924
|
throw new JJError('BISECT_NOT_ACTIVE', 'No active bisect session');
|
|
@@ -6329,7 +6943,7 @@ export async function createJJ(options) {
|
|
|
6329
6943
|
* @returns {Promise<Object>} Updated bisect state
|
|
6330
6944
|
*/
|
|
6331
6945
|
async bad() {
|
|
6332
|
-
const bisectState = await storage.read('bisect/state.json');
|
|
6946
|
+
const bisectState = /** @type {any} */ (await storage.read('bisect/state.json'));
|
|
6333
6947
|
|
|
6334
6948
|
if (!bisectState || !bisectState.active) {
|
|
6335
6949
|
throw new JJError('BISECT_NOT_ACTIVE', 'No active bisect session');
|
|
@@ -6354,7 +6968,7 @@ export async function createJJ(options) {
|
|
|
6354
6968
|
* @returns {Promise<Object>} Updated bisect state
|
|
6355
6969
|
*/
|
|
6356
6970
|
async skip() {
|
|
6357
|
-
const bisectState = await storage.read('bisect/state.json');
|
|
6971
|
+
const bisectState = /** @type {any} */ (await storage.read('bisect/state.json'));
|
|
6358
6972
|
|
|
6359
6973
|
if (!bisectState || !bisectState.active) {
|
|
6360
6974
|
throw new JJError('BISECT_NOT_ACTIVE', 'No active bisect session');
|
|
@@ -6362,7 +6976,9 @@ export async function createJJ(options) {
|
|
|
6362
6976
|
|
|
6363
6977
|
// Remove current from candidates
|
|
6364
6978
|
if (bisectState.current) {
|
|
6365
|
-
bisectState.candidates = bisectState.candidates.filter(
|
|
6979
|
+
bisectState.candidates = bisectState.candidates.filter(
|
|
6980
|
+
(/** @type {any} */ c) => c !== bisectState.current
|
|
6981
|
+
);
|
|
6366
6982
|
}
|
|
6367
6983
|
|
|
6368
6984
|
// Pick next candidate
|
|
@@ -6389,7 +7005,7 @@ export async function createJJ(options) {
|
|
|
6389
7005
|
* @returns {Promise<Object>} Bisect state
|
|
6390
7006
|
*/
|
|
6391
7007
|
async status() {
|
|
6392
|
-
const bisectState = await storage.read('bisect/state.json');
|
|
7008
|
+
const bisectState = /** @type {any} */ (await storage.read('bisect/state.json'));
|
|
6393
7009
|
return bisectState || { active: false };
|
|
6394
7010
|
},
|
|
6395
7011
|
|
|
@@ -6406,7 +7022,7 @@ export async function createJJ(options) {
|
|
|
6406
7022
|
|
|
6407
7023
|
// Find all ancestors of bad that are descendants of good
|
|
6408
7024
|
const badAncestors = graph.getAncestors(badId);
|
|
6409
|
-
const
|
|
7025
|
+
const _goodAncestors = graph.getAncestors(goodId);
|
|
6410
7026
|
|
|
6411
7027
|
// Candidates are changes that are:
|
|
6412
7028
|
// - Ancestors of bad (or bad itself)
|
|
@@ -6436,7 +7052,7 @@ export async function createJJ(options) {
|
|
|
6436
7052
|
* Update bisect state after marking good/bad/skip
|
|
6437
7053
|
*
|
|
6438
7054
|
* @private
|
|
6439
|
-
* @param {
|
|
7055
|
+
* @param {any} bisectState - Current bisect state
|
|
6440
7056
|
*/
|
|
6441
7057
|
async _updateBisectState(bisectState) {
|
|
6442
7058
|
await graph.load();
|
|
@@ -6482,7 +7098,8 @@ export async function createJJ(options) {
|
|
|
6482
7098
|
}
|
|
6483
7099
|
|
|
6484
7100
|
bisectState.candidates = newCandidates;
|
|
6485
|
-
bisectState.remaining =
|
|
7101
|
+
bisectState.remaining =
|
|
7102
|
+
newCandidates.length > 0 ? Math.ceil(Math.log2(newCandidates.length + 1)) : 0;
|
|
6486
7103
|
|
|
6487
7104
|
// Pick the middle candidate
|
|
6488
7105
|
if (newCandidates.length > 0) {
|
|
@@ -6495,7 +7112,7 @@ export async function createJJ(options) {
|
|
|
6495
7112
|
// The first bad change is the one in the bad set that has no bad ancestors
|
|
6496
7113
|
for (const badId of bisectState.bad) {
|
|
6497
7114
|
const badAncestors = graph.getAncestors(badId);
|
|
6498
|
-
const hasChildBad = bisectState.bad.some(otherId => {
|
|
7115
|
+
const hasChildBad = bisectState.bad.some((/** @type {any} */ otherId) => {
|
|
6499
7116
|
if (otherId === badId) return false;
|
|
6500
7117
|
return badAncestors.includes(otherId);
|
|
6501
7118
|
});
|
|
@@ -6512,13 +7129,16 @@ export async function createJJ(options) {
|
|
|
6512
7129
|
/**
|
|
6513
7130
|
* Show file differences between revisions (matches `jj diff`)
|
|
6514
7131
|
*
|
|
6515
|
-
* @param {
|
|
6516
|
-
* @param {string} [args.from] - Source revision (defaults to parent of working copy)
|
|
6517
|
-
* @param {string} [args.to] - Target revision (defaults to working copy)
|
|
6518
|
-
* @param {string[]} [args.paths] - Specific paths to diff
|
|
7132
|
+
* @param {Record<string, any>} [args={}] - Diff arguments
|
|
6519
7133
|
* @returns {Promise<Object>} Diff result with changed files
|
|
6520
7134
|
*/
|
|
6521
7135
|
async diff(args = {}) {
|
|
7136
|
+
// v1.6: snapshot the working copy first so diffs against @ reflect
|
|
7137
|
+
// out-of-band edits (only when diffing the working copy, i.e. no explicit `to`).
|
|
7138
|
+
if (!args.to) {
|
|
7139
|
+
await autoSnapshotWorkingCopy();
|
|
7140
|
+
}
|
|
7141
|
+
|
|
6522
7142
|
await graph.load();
|
|
6523
7143
|
await workingCopy.load();
|
|
6524
7144
|
|
|
@@ -6574,8 +7194,7 @@ export async function createJJ(options) {
|
|
|
6574
7194
|
/**
|
|
6575
7195
|
* Move working copy to next child revision (matches `jj next`)
|
|
6576
7196
|
*
|
|
6577
|
-
* @param {
|
|
6578
|
-
* @param {number} [args.offset=1] - Number of generations to move forward
|
|
7197
|
+
* @param {Record<string, any>} [args={}] - Next arguments
|
|
6579
7198
|
* @returns {Promise<Object>} Updated working copy info
|
|
6580
7199
|
*/
|
|
6581
7200
|
async next(args = {}) {
|
|
@@ -6593,7 +7212,9 @@ export async function createJJ(options) {
|
|
|
6593
7212
|
// Find children
|
|
6594
7213
|
await graph.load();
|
|
6595
7214
|
const allChanges = await graph.getAllChanges();
|
|
6596
|
-
const children = allChanges.filter(
|
|
7215
|
+
const children = allChanges.filter(
|
|
7216
|
+
(/** @type {any} */ c) => c.parents && c.parents.includes(currentChangeId)
|
|
7217
|
+
);
|
|
6597
7218
|
|
|
6598
7219
|
if (children.length === 0) {
|
|
6599
7220
|
throw new JJError('NO_CHILDREN', 'No child revisions found');
|
|
@@ -6604,7 +7225,9 @@ export async function createJJ(options) {
|
|
|
6604
7225
|
|
|
6605
7226
|
// Move forward 'offset' times
|
|
6606
7227
|
for (let i = 1; i < offset; i++) {
|
|
6607
|
-
const nextChildren = allChanges.filter(
|
|
7228
|
+
const nextChildren = allChanges.filter(
|
|
7229
|
+
(/** @type {any} */ c) => c.parents && c.parents.includes(targetChange.changeId)
|
|
7230
|
+
);
|
|
6608
7231
|
if (nextChildren.length === 0) {
|
|
6609
7232
|
throw new JJError('INSUFFICIENT_CHILDREN', `Cannot move forward ${offset} generations`);
|
|
6610
7233
|
}
|
|
@@ -6625,8 +7248,7 @@ export async function createJJ(options) {
|
|
|
6625
7248
|
/**
|
|
6626
7249
|
* Move working copy to previous parent revision (matches `jj prev`)
|
|
6627
7250
|
*
|
|
6628
|
-
* @param {
|
|
6629
|
-
* @param {number} [args.offset=1] - Number of generations to move backward
|
|
7251
|
+
* @param {Record<string, any>} [args={}] - Prev arguments
|
|
6630
7252
|
* @returns {Promise<Object>} Updated working copy info
|
|
6631
7253
|
*/
|
|
6632
7254
|
async prev(args = {}) {
|
|
@@ -6665,9 +7287,7 @@ export async function createJJ(options) {
|
|
|
6665
7287
|
/**
|
|
6666
7288
|
* Create copies of changes (matches `jj duplicate`)
|
|
6667
7289
|
*
|
|
6668
|
-
* @param {
|
|
6669
|
-
* @param {string[]} [args.changes] - Change IDs to duplicate (defaults to working copy)
|
|
6670
|
-
* @param {string} [args.destination] - Where to place duplicates
|
|
7290
|
+
* @param {Record<string, any>} [args={}] - Duplicate arguments
|
|
6671
7291
|
* @returns {Promise<Object>} Duplication result with new change IDs
|
|
6672
7292
|
*/
|
|
6673
7293
|
async duplicate(args = {}) {
|
|
@@ -6676,8 +7296,8 @@ export async function createJJ(options) {
|
|
|
6676
7296
|
await userConfig.load();
|
|
6677
7297
|
|
|
6678
7298
|
// Support both changeId (singular) and changes (plural)
|
|
6679
|
-
const changesToDup =
|
|
6680
|
-
|
|
7299
|
+
const changesToDup =
|
|
7300
|
+
args.changes || (args.changeId ? [args.changeId] : [workingCopy.getCurrentChangeId()]);
|
|
6681
7301
|
const duplicatedChanges = [];
|
|
6682
7302
|
|
|
6683
7303
|
for (const changeId of changesToDup) {
|
|
@@ -6688,7 +7308,7 @@ export async function createJJ(options) {
|
|
|
6688
7308
|
|
|
6689
7309
|
// Create new change as a copy
|
|
6690
7310
|
const newChangeId = generateChangeId();
|
|
6691
|
-
const
|
|
7311
|
+
const _user = /** @type {any} */ (userConfig.getUser());
|
|
6692
7312
|
const newChange = {
|
|
6693
7313
|
...originalChange,
|
|
6694
7314
|
changeId: newChangeId,
|
|
@@ -6713,7 +7333,7 @@ export async function createJJ(options) {
|
|
|
6713
7333
|
view: {
|
|
6714
7334
|
bookmarks: {},
|
|
6715
7335
|
remoteBookmarks: {},
|
|
6716
|
-
heads: duplicatedChanges.map(d => d.duplicate),
|
|
7336
|
+
heads: duplicatedChanges.map((d) => d.duplicate),
|
|
6717
7337
|
workingCopy: workingCopy.getCurrentChangeId(),
|
|
6718
7338
|
},
|
|
6719
7339
|
});
|
|
@@ -6721,17 +7341,14 @@ export async function createJJ(options) {
|
|
|
6721
7341
|
// Return both formats for compatibility
|
|
6722
7342
|
return {
|
|
6723
7343
|
duplicated: duplicatedChanges,
|
|
6724
|
-
changeIds: duplicatedChanges.map(d => d.duplicate)
|
|
7344
|
+
changeIds: duplicatedChanges.map((d) => d.duplicate),
|
|
6725
7345
|
};
|
|
6726
7346
|
},
|
|
6727
7347
|
|
|
6728
7348
|
/**
|
|
6729
7349
|
* Restore paths from another revision (matches `jj restore`)
|
|
6730
7350
|
*
|
|
6731
|
-
* @param {
|
|
6732
|
-
* @param {string} [args.from] - Source revision to restore from
|
|
6733
|
-
* @param {string} [args.to] - Target revision to restore to (defaults to working copy)
|
|
6734
|
-
* @param {string[]} [args.paths] - Specific paths to restore (defaults to all)
|
|
7351
|
+
* @param {Record<string, any>} args - Restore arguments
|
|
6735
7352
|
* @returns {Promise<Object>} Restore result
|
|
6736
7353
|
*/
|
|
6737
7354
|
async restore(args = {}) {
|
|
@@ -6793,9 +7410,7 @@ export async function createJJ(options) {
|
|
|
6793
7410
|
* This operation takes multiple changes and gives them the same parent,
|
|
6794
7411
|
* making them siblings in the change graph instead of ancestors/descendants.
|
|
6795
7412
|
*
|
|
6796
|
-
* @param {
|
|
6797
|
-
* @param {Array<string>} args.changes - Change IDs to parallelize
|
|
6798
|
-
* @param {string} [args.parent] - Optional parent to use (defaults to common ancestor)
|
|
7413
|
+
* @param {Record<string, any>} args - Arguments
|
|
6799
7414
|
* @returns {Promise<Object>} Operation result with parallelized changes
|
|
6800
7415
|
*
|
|
6801
7416
|
* @example
|
|
@@ -6912,7 +7527,7 @@ export async function createJJ(options) {
|
|
|
6912
7527
|
parent: parentId,
|
|
6913
7528
|
};
|
|
6914
7529
|
},
|
|
6915
|
-
};
|
|
7530
|
+
});
|
|
6916
7531
|
|
|
6917
7532
|
// ============================================================================
|
|
6918
7533
|
// UNSUPPORTED FEATURE STUBS
|
|
@@ -6925,17 +7540,14 @@ export async function createJJ(options) {
|
|
|
6925
7540
|
* @status UNSUPPORTED - Requires interactive diff editor
|
|
6926
7541
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
6927
7542
|
*/
|
|
6928
|
-
jj.diffedit = async function (
|
|
6929
|
-
throw new JJError(
|
|
6930
|
-
'
|
|
6931
|
-
'
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
|
|
6936
|
-
suggestion: 'For programmatic editing, use jj.write() to update files and jj.describe() to update the description',
|
|
6937
|
-
}
|
|
6938
|
-
);
|
|
7543
|
+
jj.diffedit = async function (/** @type {any} */ _args) {
|
|
7544
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'diffedit requires interactive diff editor', {
|
|
7545
|
+
feature: 'diffedit',
|
|
7546
|
+
reason: 'Interactive tools not supported in library API',
|
|
7547
|
+
alternative: 'Use jj.diff() to view changes, then jj.edit() to modify the change',
|
|
7548
|
+
suggestion:
|
|
7549
|
+
'For programmatic editing, use jj.write() to update files and jj.describe() to update the description',
|
|
7550
|
+
});
|
|
6939
7551
|
};
|
|
6940
7552
|
|
|
6941
7553
|
/**
|
|
@@ -6944,55 +7556,19 @@ export async function createJJ(options) {
|
|
|
6944
7556
|
* @status UNSUPPORTED - Requires external formatters
|
|
6945
7557
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
6946
7558
|
*/
|
|
6947
|
-
jj.fix = async function (
|
|
6948
|
-
throw new JJError(
|
|
6949
|
-
'
|
|
6950
|
-
'
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
alternative: 'Run formatters separately, then use jj.write() to update files',
|
|
6955
|
-
suggestion: 'Consider using pre-commit hooks to run formatters automatically',
|
|
6956
|
-
}
|
|
6957
|
-
);
|
|
6958
|
-
};
|
|
6959
|
-
|
|
6960
|
-
/**
|
|
6961
|
-
* sign - Cryptographically sign a revision (STUB)
|
|
6962
|
-
*
|
|
6963
|
-
* @status UNSUPPORTED - Cryptographic signing not implemented
|
|
6964
|
-
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
6965
|
-
*/
|
|
6966
|
-
jj.sign = async function (args) {
|
|
6967
|
-
throw new JJError(
|
|
6968
|
-
'UNSUPPORTED_OPERATION',
|
|
6969
|
-
'Cryptographic signing not implemented',
|
|
6970
|
-
{
|
|
6971
|
-
feature: 'sign',
|
|
6972
|
-
reason: 'GPG/SSH signing requires complex key management and security considerations',
|
|
6973
|
-
alternative: 'Use Git\'s signing features with jj.git.push() for signed commits',
|
|
6974
|
-
suggestion: 'Sign commits at the Git layer using git commit --gpg-sign',
|
|
6975
|
-
}
|
|
6976
|
-
);
|
|
7559
|
+
jj.fix = async function (/** @type {any} */ _args) {
|
|
7560
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'Automatic formatting requires external tools', {
|
|
7561
|
+
feature: 'fix',
|
|
7562
|
+
reason: 'External formatters (prettier, eslint, etc.) must be integrated separately',
|
|
7563
|
+
alternative: 'Run formatters separately, then use jj.write() to update files',
|
|
7564
|
+
suggestion: 'Consider using pre-commit hooks to run formatters automatically',
|
|
7565
|
+
});
|
|
6977
7566
|
};
|
|
6978
7567
|
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
6984
|
-
*/
|
|
6985
|
-
jj.unsign = async function (args) {
|
|
6986
|
-
throw new JJError(
|
|
6987
|
-
'UNSUPPORTED_OPERATION',
|
|
6988
|
-
'Cryptographic signing not implemented',
|
|
6989
|
-
{
|
|
6990
|
-
feature: 'unsign',
|
|
6991
|
-
reason: 'Signing features not implemented in isomorphic-jj',
|
|
6992
|
-
alternative: 'Modify commits using jj.metaedit() if you need to change metadata',
|
|
6993
|
-
}
|
|
6994
|
-
);
|
|
6995
|
-
};
|
|
7568
|
+
// NOTE: jj.sign() / jj.unsign() are implemented as real (metadata-based)
|
|
7569
|
+
// methods in the main API object above (v1.5). The old UNSUPPORTED stubs were
|
|
7570
|
+
// removed — a pure-JS library can't verify keys, but it can record and expose
|
|
7571
|
+
// signature metadata for the signed() revset and Git-layer integration.
|
|
6996
7572
|
|
|
6997
7573
|
/**
|
|
6998
7574
|
* util - CLI utility commands (STUB namespace)
|
|
@@ -7005,15 +7581,11 @@ export async function createJJ(options) {
|
|
|
7005
7581
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
7006
7582
|
*/
|
|
7007
7583
|
async completion() {
|
|
7008
|
-
throw new JJError(
|
|
7009
|
-
'
|
|
7010
|
-
'
|
|
7011
|
-
|
|
7012
|
-
|
|
7013
|
-
reason: 'Completions are for command-line shells, not library APIs',
|
|
7014
|
-
alternative: 'Not applicable - use JJ CLI for shell completions',
|
|
7015
|
-
}
|
|
7016
|
-
);
|
|
7584
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'Shell completion generation is CLI-specific', {
|
|
7585
|
+
feature: 'util.completion',
|
|
7586
|
+
reason: 'Completions are for command-line shells, not library APIs',
|
|
7587
|
+
alternative: 'Not applicable - use JJ CLI for shell completions',
|
|
7588
|
+
});
|
|
7017
7589
|
},
|
|
7018
7590
|
|
|
7019
7591
|
/**
|
|
@@ -7021,31 +7593,23 @@ export async function createJJ(options) {
|
|
|
7021
7593
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
7022
7594
|
*/
|
|
7023
7595
|
async gc() {
|
|
7024
|
-
throw new JJError(
|
|
7025
|
-
'
|
|
7026
|
-
'
|
|
7027
|
-
|
|
7028
|
-
|
|
7029
|
-
reason: 'Storage management handled automatically by isomorphic-jj',
|
|
7030
|
-
alternative: 'Repository cleanup happens automatically',
|
|
7031
|
-
}
|
|
7032
|
-
);
|
|
7596
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'Garbage collection is a CLI utility', {
|
|
7597
|
+
feature: 'util.gc',
|
|
7598
|
+
reason: 'Storage management handled automatically by isomorphic-jj',
|
|
7599
|
+
alternative: 'Repository cleanup happens automatically',
|
|
7600
|
+
});
|
|
7033
7601
|
},
|
|
7034
7602
|
|
|
7035
7603
|
/**
|
|
7036
7604
|
* exec - Execute command with JJ environment (STUB)
|
|
7037
7605
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
7038
7606
|
*/
|
|
7039
|
-
async exec(
|
|
7040
|
-
throw new JJError(
|
|
7041
|
-
'
|
|
7042
|
-
'
|
|
7043
|
-
|
|
7044
|
-
|
|
7045
|
-
reason: 'Shell command execution not appropriate for library API',
|
|
7046
|
-
alternative: 'Use Node.js child_process module directly if needed',
|
|
7047
|
-
}
|
|
7048
|
-
);
|
|
7607
|
+
async exec(/** @type {any} */ _args) {
|
|
7608
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'Command execution is a CLI utility', {
|
|
7609
|
+
feature: 'util.exec',
|
|
7610
|
+
reason: 'Shell command execution not appropriate for library API',
|
|
7611
|
+
alternative: 'Use Node.js child_process module directly if needed',
|
|
7612
|
+
});
|
|
7049
7613
|
},
|
|
7050
7614
|
|
|
7051
7615
|
/**
|
|
@@ -7053,15 +7617,11 @@ export async function createJJ(options) {
|
|
|
7053
7617
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
7054
7618
|
*/
|
|
7055
7619
|
async configSchema() {
|
|
7056
|
-
throw new JJError(
|
|
7057
|
-
'
|
|
7058
|
-
'
|
|
7059
|
-
|
|
7060
|
-
|
|
7061
|
-
reason: 'Schema introspection not needed for programmatic usage',
|
|
7062
|
-
alternative: 'See TypeScript types or API documentation for config structure',
|
|
7063
|
-
}
|
|
7064
|
-
);
|
|
7620
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'Config schema output is a CLI utility', {
|
|
7621
|
+
feature: 'util.configSchema',
|
|
7622
|
+
reason: 'Schema introspection not needed for programmatic usage',
|
|
7623
|
+
alternative: 'See TypeScript types or API documentation for config structure',
|
|
7624
|
+
});
|
|
7065
7625
|
},
|
|
7066
7626
|
};
|
|
7067
7627
|
|
|
@@ -7075,17 +7635,13 @@ export async function createJJ(options) {
|
|
|
7075
7635
|
* upload - Upload changes to Gerrit (STUB)
|
|
7076
7636
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
7077
7637
|
*/
|
|
7078
|
-
async upload(
|
|
7079
|
-
throw new JJError(
|
|
7080
|
-
'
|
|
7081
|
-
'Gerrit
|
|
7082
|
-
|
|
7083
|
-
|
|
7084
|
-
|
|
7085
|
-
alternative: 'Use jj.git.push() with appropriate Gerrit remote configuration',
|
|
7086
|
-
suggestion: 'Configure Gerrit remote and use standard Git push with refs/for/ syntax',
|
|
7087
|
-
}
|
|
7088
|
-
);
|
|
7638
|
+
async upload(/** @type {any} */ _args) {
|
|
7639
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'Gerrit integration not supported', {
|
|
7640
|
+
feature: 'gerrit.upload',
|
|
7641
|
+
reason: 'Gerrit-specific features not implemented',
|
|
7642
|
+
alternative: 'Use jj.git.push() with appropriate Gerrit remote configuration',
|
|
7643
|
+
suggestion: 'Configure Gerrit remote and use standard Git push with refs/for/ syntax',
|
|
7644
|
+
});
|
|
7089
7645
|
},
|
|
7090
7646
|
};
|
|
7091
7647
|
|