isomorphic-jj 1.4.3 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +226 -0
- package/README.md +9 -11
- package/bin/isojj.js +270 -102
- package/dist/index.cjs +2910 -1666
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +2910 -1666
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -4
- package/src/api/repository.js +1099 -739
- package/src/backend/isomorphic-git-backend.js +183 -136
- package/src/backend/lazy-git-backend.js +12 -19
- package/src/browser/helpers.js +6 -2
- package/src/core/background-ops.js +58 -41
- package/src/core/bookmark-store.js +12 -8
- package/src/core/change-graph.js +26 -29
- package/src/core/conflict-model.js +66 -26
- package/src/core/jj-checkout.js +8 -4
- package/src/core/jj-operation-store.js +16 -12
- package/src/core/jj-tree-state.js +13 -9
- package/src/core/jj-view-store.js +11 -5
- package/src/core/merge-driver-registry.js +92 -46
- package/src/core/operation-log.js +30 -33
- package/src/core/revset-engine.js +1069 -485
- package/src/core/storage-manager.js +25 -19
- package/src/core/tag-store.js +2 -2
- package/src/core/user-config.js +3 -1
- package/src/core/working-copy.js +125 -9
- package/src/core/workspace-manager.js +2 -2
- package/src/drivers/built-in-drivers.js +66 -33
- package/src/types.d.ts +67 -1
- package/src/utils/errors.js +3 -3
- package/src/utils/id-generation.js +4 -4
- package/src/utils/validation.js +6 -10
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`, {
|
|
@@ -1093,6 +1179,7 @@ export async function createJJ(options) {
|
|
|
1093
1179
|
if (isWorkingCopy && !args.preserveSnapshot) {
|
|
1094
1180
|
// Snapshot current file contents for conflict detection
|
|
1095
1181
|
// This allows us to load file contents during merge/rebase
|
|
1182
|
+
/** @type {Record<string, any>} */
|
|
1096
1183
|
const fileSnapshot = {};
|
|
1097
1184
|
const trackedFiles = await workingCopy.listFiles();
|
|
1098
1185
|
|
|
@@ -1124,7 +1211,11 @@ export async function createJJ(options) {
|
|
|
1124
1211
|
throw new JJError(
|
|
1125
1212
|
'SNAPSHOT_FILE_FAILED',
|
|
1126
1213
|
`Could not snapshot file ${filePath}: ${error.message}`,
|
|
1127
|
-
{
|
|
1214
|
+
{
|
|
1215
|
+
filePath,
|
|
1216
|
+
originalError: error.message,
|
|
1217
|
+
suggestion: 'File may be binary, deleted, or inaccessible',
|
|
1218
|
+
}
|
|
1128
1219
|
);
|
|
1129
1220
|
}
|
|
1130
1221
|
}
|
|
@@ -1154,12 +1245,17 @@ export async function createJJ(options) {
|
|
|
1154
1245
|
});
|
|
1155
1246
|
|
|
1156
1247
|
// Dispatch change:updated event (informational)
|
|
1157
|
-
await dispatchEventAsync(
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1248
|
+
await dispatchEventAsync(
|
|
1249
|
+
jj,
|
|
1250
|
+
'change:updated',
|
|
1251
|
+
{
|
|
1252
|
+
operation: 'describe',
|
|
1253
|
+
changeId: targetChangeId,
|
|
1254
|
+
change,
|
|
1255
|
+
timestamp: new Date().toISOString(),
|
|
1256
|
+
},
|
|
1257
|
+
{ cancelable: false }
|
|
1258
|
+
);
|
|
1163
1259
|
|
|
1164
1260
|
return change;
|
|
1165
1261
|
},
|
|
@@ -1170,18 +1266,7 @@ export async function createJJ(options) {
|
|
|
1170
1266
|
* Allows updating author, committer, description, and optionally regenerating the change ID.
|
|
1171
1267
|
* The file content and snapshot remain unchanged.
|
|
1172
1268
|
*
|
|
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
|
|
1269
|
+
* @param {Record<string, any>} [args={}] - Arguments
|
|
1185
1270
|
* @returns {Promise<Object>} Updated change object
|
|
1186
1271
|
*/
|
|
1187
1272
|
async metaedit(args = {}) {
|
|
@@ -1202,7 +1287,7 @@ export async function createJJ(options) {
|
|
|
1202
1287
|
// Determine which change to edit
|
|
1203
1288
|
const changeId = args.revision || workingCopy.getCurrentChangeId();
|
|
1204
1289
|
const change = await graph.getChange(changeId);
|
|
1205
|
-
const user = userConfig.getUser();
|
|
1290
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
1206
1291
|
|
|
1207
1292
|
if (!change) {
|
|
1208
1293
|
throw new JJError('CHANGE_NOT_FOUND', `Change ${changeId} not found`);
|
|
@@ -1215,17 +1300,13 @@ export async function createJJ(options) {
|
|
|
1215
1300
|
const hasDescription = args.description !== undefined;
|
|
1216
1301
|
|
|
1217
1302
|
if (!hasAuthor && !hasCommitter && !hasResetChangeId && !hasDescription) {
|
|
1218
|
-
throw new JJError(
|
|
1219
|
-
'
|
|
1220
|
-
|
|
1221
|
-
{
|
|
1222
|
-
suggestion: 'Provide author, committer, description, or resetChangeId: true',
|
|
1223
|
-
}
|
|
1224
|
-
);
|
|
1303
|
+
throw new JJError('INVALID_ARGUMENT', 'No metadata provided to update', {
|
|
1304
|
+
suggestion: 'Provide author, committer, description, or resetChangeId: true',
|
|
1305
|
+
});
|
|
1225
1306
|
}
|
|
1226
1307
|
|
|
1227
1308
|
// Store old change ID for potential regeneration
|
|
1228
|
-
const
|
|
1309
|
+
const _oldChangeId = change.changeId;
|
|
1229
1310
|
|
|
1230
1311
|
// Update author if provided
|
|
1231
1312
|
if (args.author) {
|
|
@@ -1276,15 +1357,11 @@ export async function createJJ(options) {
|
|
|
1276
1357
|
// - Updating all children to point to new change ID
|
|
1277
1358
|
// - Updating all bookmarks pointing to old change ID
|
|
1278
1359
|
// - 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
|
-
);
|
|
1360
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'resetChangeId is not yet implemented', {
|
|
1361
|
+
feature: 'metaedit resetChangeId',
|
|
1362
|
+
reason: 'Requires complex graph manipulation to update all references',
|
|
1363
|
+
suggestion: 'Create a new change with desired metadata instead',
|
|
1364
|
+
});
|
|
1288
1365
|
}
|
|
1289
1366
|
|
|
1290
1367
|
// Update the existing change
|
|
@@ -1310,12 +1387,7 @@ export async function createJJ(options) {
|
|
|
1310
1387
|
/**
|
|
1311
1388
|
* Create a new change
|
|
1312
1389
|
*
|
|
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
|
|
1390
|
+
* @param {Record<string, any>} [args={}] - Arguments
|
|
1319
1391
|
* @returns {Promise<Object>} The new change object including changeId, description, parents, author, and timestamp
|
|
1320
1392
|
*/
|
|
1321
1393
|
async new(args = {}) {
|
|
@@ -1357,7 +1429,7 @@ export async function createJJ(options) {
|
|
|
1357
1429
|
}
|
|
1358
1430
|
|
|
1359
1431
|
const newChangeId = generateChangeId();
|
|
1360
|
-
const user = userConfig.getUser();
|
|
1432
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
1361
1433
|
|
|
1362
1434
|
// Initialize with parent's file snapshot
|
|
1363
1435
|
let initialSnapshot = {};
|
|
@@ -1398,7 +1470,7 @@ export async function createJJ(options) {
|
|
|
1398
1470
|
timestamp: new Date().toISOString(),
|
|
1399
1471
|
});
|
|
1400
1472
|
|
|
1401
|
-
await graph.addChange(newChange);
|
|
1473
|
+
await graph.addChange(newChange); // Middleware will sync to Git
|
|
1402
1474
|
|
|
1403
1475
|
// Handle insertBefore: rebase target to have new change as parent
|
|
1404
1476
|
if (args.insertBefore) {
|
|
@@ -1429,17 +1501,22 @@ export async function createJJ(options) {
|
|
|
1429
1501
|
});
|
|
1430
1502
|
|
|
1431
1503
|
// Dispatch change:created event (informational)
|
|
1432
|
-
await dispatchEventAsync(
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1504
|
+
await dispatchEventAsync(
|
|
1505
|
+
jj,
|
|
1506
|
+
'change:created',
|
|
1507
|
+
{
|
|
1508
|
+
operation: 'new',
|
|
1509
|
+
changeId: newChangeId,
|
|
1510
|
+
parents: parents,
|
|
1511
|
+
change: newChange,
|
|
1512
|
+
timestamp: new Date().toISOString(),
|
|
1513
|
+
},
|
|
1514
|
+
{ cancelable: false }
|
|
1515
|
+
);
|
|
1439
1516
|
|
|
1440
1517
|
return newChange;
|
|
1441
1518
|
},
|
|
1442
|
-
|
|
1519
|
+
|
|
1443
1520
|
/**
|
|
1444
1521
|
* Get working copy status
|
|
1445
1522
|
*/
|
|
@@ -1448,21 +1525,57 @@ export async function createJJ(options) {
|
|
|
1448
1525
|
await workingCopy.load();
|
|
1449
1526
|
await conflicts.load();
|
|
1450
1527
|
|
|
1528
|
+
// Dirty tracked files (computed before reconciling so modifications to
|
|
1529
|
+
// already-tracked files are still reported).
|
|
1530
|
+
const modified = await workingCopy.getModifiedFiles();
|
|
1531
|
+
|
|
1532
|
+
// v1.6: snapshot the working directory so files added/removed on disk
|
|
1533
|
+
// out-of-band show up in status.
|
|
1534
|
+
const snap = await autoSnapshotWorkingCopy();
|
|
1535
|
+
|
|
1451
1536
|
const currentChangeId = workingCopy.getCurrentChangeId();
|
|
1452
1537
|
const change = await graph.getChange(currentChangeId);
|
|
1453
|
-
const modified = await workingCopy.getModifiedFiles();
|
|
1454
1538
|
// Only show unresolved conflicts in status
|
|
1455
1539
|
const activeConflicts = conflicts.listConflicts({ resolved: false });
|
|
1456
1540
|
|
|
1457
1541
|
return {
|
|
1458
1542
|
workingCopy: change,
|
|
1459
1543
|
modified,
|
|
1460
|
-
added: [],
|
|
1461
|
-
removed: [],
|
|
1544
|
+
added: snap ? snap.added : [],
|
|
1545
|
+
removed: snap ? snap.deleted : [],
|
|
1462
1546
|
conflicts: activeConflicts,
|
|
1463
1547
|
};
|
|
1464
1548
|
},
|
|
1465
1549
|
|
|
1550
|
+
/**
|
|
1551
|
+
* Explicitly snapshot the working directory (v1.6).
|
|
1552
|
+
*
|
|
1553
|
+
* Walks the working copy on disk and reconciles tracked file state and the
|
|
1554
|
+
* working-copy change's content, picking up files created/modified/deleted
|
|
1555
|
+
* outside of jj.write() (editor, shell, git checkout). This runs
|
|
1556
|
+
* automatically before status/describe/diff/read/file.* unless the repo was
|
|
1557
|
+
* created with `{ autoSnapshot: false }`; call this to trigger it manually.
|
|
1558
|
+
*
|
|
1559
|
+
* @returns {Promise<{added: string[], modified: string[], deleted: string[]}>}
|
|
1560
|
+
*/
|
|
1561
|
+
async snapshot() {
|
|
1562
|
+
await graph.load();
|
|
1563
|
+
await workingCopy.load();
|
|
1564
|
+
const result = await workingCopy.snapshot();
|
|
1565
|
+
// Refresh the working-copy change's content snapshot from disk.
|
|
1566
|
+
try {
|
|
1567
|
+
const wcId = workingCopy.getCurrentChangeId();
|
|
1568
|
+
const change = await graph.getChange(wcId);
|
|
1569
|
+
if (change) {
|
|
1570
|
+
change.fileSnapshot = await snapshotFilesystem();
|
|
1571
|
+
await graph.updateChange(change);
|
|
1572
|
+
}
|
|
1573
|
+
} catch {
|
|
1574
|
+
// Best-effort content refresh.
|
|
1575
|
+
}
|
|
1576
|
+
return result;
|
|
1577
|
+
},
|
|
1578
|
+
|
|
1466
1579
|
/**
|
|
1467
1580
|
* Get repository statistics and analytics
|
|
1468
1581
|
*
|
|
@@ -1477,9 +1590,10 @@ export async function createJJ(options) {
|
|
|
1477
1590
|
|
|
1478
1591
|
const all = graph.getAll();
|
|
1479
1592
|
const ops = await oplog.list();
|
|
1480
|
-
const currentUser = userConfig.getUser();
|
|
1593
|
+
const currentUser = /** @type {any} */ (userConfig.getUser());
|
|
1481
1594
|
|
|
1482
1595
|
// Count by author
|
|
1596
|
+
/** @type {Record<string, any>} */
|
|
1483
1597
|
const authorCounts = {};
|
|
1484
1598
|
for (const change of all) {
|
|
1485
1599
|
if (!change.abandoned && change.author) {
|
|
@@ -1499,26 +1613,30 @@ export async function createJJ(options) {
|
|
|
1499
1613
|
}
|
|
1500
1614
|
|
|
1501
1615
|
// Count merge commits
|
|
1502
|
-
const mergeCommits = all.filter(
|
|
1616
|
+
const mergeCommits = all.filter(
|
|
1617
|
+
(/** @type {any} */ c) => c.parents && c.parents.length > 1
|
|
1618
|
+
).length;
|
|
1503
1619
|
|
|
1504
1620
|
// Count empty commits
|
|
1505
|
-
const emptyCommits = all.filter(
|
|
1506
|
-
c.tree === '0000000000000000000000000000000000000000'
|
|
1621
|
+
const emptyCommits = all.filter(
|
|
1622
|
+
(/** @type {any} */ c) => c.tree === '0000000000000000000000000000000000000000'
|
|
1507
1623
|
).length;
|
|
1508
1624
|
|
|
1509
1625
|
// Count abandoned
|
|
1510
|
-
const abandonedCount = all.filter(c => c.abandoned).length;
|
|
1626
|
+
const abandonedCount = all.filter((/** @type {any} */ c) => c.abandoned).length;
|
|
1511
1627
|
|
|
1512
1628
|
// My commits
|
|
1513
|
-
const myCommits = all.filter(
|
|
1514
|
-
|
|
1515
|
-
|
|
1629
|
+
const myCommits = all.filter(
|
|
1630
|
+
(/** @type {any} */ c) =>
|
|
1631
|
+
c.author &&
|
|
1632
|
+
!c.abandoned &&
|
|
1633
|
+
(c.author.email === currentUser.email || c.author.name === currentUser.name)
|
|
1516
1634
|
).length;
|
|
1517
1635
|
|
|
1518
1636
|
// Get bookmark statistics
|
|
1519
1637
|
const allBookmarks = await bookmarks.list();
|
|
1520
|
-
const localBookmarks = allBookmarks.filter(b => !b.remote);
|
|
1521
|
-
const remoteBookmarks = allBookmarks.filter(b => b.remote);
|
|
1638
|
+
const localBookmarks = allBookmarks.filter((b) => !b.remote);
|
|
1639
|
+
const remoteBookmarks = allBookmarks.filter((b) => b.remote);
|
|
1522
1640
|
|
|
1523
1641
|
return {
|
|
1524
1642
|
changes: {
|
|
@@ -1537,7 +1655,7 @@ export async function createJJ(options) {
|
|
|
1537
1655
|
files: {
|
|
1538
1656
|
total: allFiles.size,
|
|
1539
1657
|
list: Array.from(allFiles).sort(),
|
|
1540
|
-
byExtension: {},
|
|
1658
|
+
byExtension: {}, // Add file extension breakdown
|
|
1541
1659
|
},
|
|
1542
1660
|
bookmarks: {
|
|
1543
1661
|
total: allBookmarks.length,
|
|
@@ -1558,11 +1676,8 @@ export async function createJJ(options) {
|
|
|
1558
1676
|
/**
|
|
1559
1677
|
* View change history
|
|
1560
1678
|
*
|
|
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
|
|
1679
|
+
* @param {Record<string, any>} args - Arguments
|
|
1680
|
+
* @returns {Promise<Array<any>|number>} Array of changes or count if args.count is true
|
|
1566
1681
|
*/
|
|
1567
1682
|
async log(args = {}) {
|
|
1568
1683
|
await graph.load();
|
|
@@ -1591,7 +1706,7 @@ export async function createJJ(options) {
|
|
|
1591
1706
|
const aIsParentOfB = b.parents && b.parents.includes(a.changeId);
|
|
1592
1707
|
const bIsParentOfA = a.parents && a.parents.includes(b.changeId);
|
|
1593
1708
|
|
|
1594
|
-
if (aIsParentOfB) return 1;
|
|
1709
|
+
if (aIsParentOfB) return 1; // a is parent of b, so b comes first
|
|
1595
1710
|
if (bIsParentOfA) return -1; // b is parent of a, so a comes first
|
|
1596
1711
|
|
|
1597
1712
|
// Secondary: Sort by timestamp descending (newest first)
|
|
@@ -1611,14 +1726,13 @@ export async function createJJ(options) {
|
|
|
1611
1726
|
/**
|
|
1612
1727
|
* Show detailed information about a specific change
|
|
1613
1728
|
*
|
|
1614
|
-
* @param {
|
|
1615
|
-
* @param {string} args.change - Change ID or revset to show
|
|
1729
|
+
* @param {Record<string, any>} args - Arguments
|
|
1616
1730
|
* @returns {Promise<Object>} Detailed change information
|
|
1617
1731
|
*/
|
|
1618
1732
|
async show(args) {
|
|
1619
1733
|
if (!args || !args.change) {
|
|
1620
1734
|
throw new JJError('INVALID_ARGUMENT', 'Missing change argument', {
|
|
1621
|
-
suggestion: 'Provide { change: changeId }'
|
|
1735
|
+
suggestion: 'Provide { change: changeId }',
|
|
1622
1736
|
});
|
|
1623
1737
|
}
|
|
1624
1738
|
|
|
@@ -1654,23 +1768,23 @@ export async function createJJ(options) {
|
|
|
1654
1768
|
// Find bookmarks pointing to this change
|
|
1655
1769
|
const allBookmarks = await bookmarks.list();
|
|
1656
1770
|
const changeBookmarks = allBookmarks
|
|
1657
|
-
.filter(b => b.
|
|
1658
|
-
.map(b => b.name);
|
|
1771
|
+
.filter((b) => b.changeId === change.changeId)
|
|
1772
|
+
.map((b) => b.name);
|
|
1659
1773
|
|
|
1660
1774
|
// Find operations that modified this change
|
|
1661
1775
|
const ops = await oplog.list();
|
|
1662
|
-
const relatedOps = ops.filter(
|
|
1663
|
-
op
|
|
1664
|
-
op.view
|
|
1665
|
-
(op.view.
|
|
1666
|
-
|
|
1776
|
+
const relatedOps = ops.filter(
|
|
1777
|
+
(op) =>
|
|
1778
|
+
op.view &&
|
|
1779
|
+
(op.view.workingCopy === change.changeId ||
|
|
1780
|
+
(op.view.heads && op.view.heads.includes(change.changeId)))
|
|
1667
1781
|
);
|
|
1668
1782
|
|
|
1669
1783
|
return {
|
|
1670
1784
|
...change,
|
|
1671
1785
|
children,
|
|
1672
1786
|
bookmarks: changeBookmarks,
|
|
1673
|
-
operations: relatedOps.map(op => ({
|
|
1787
|
+
operations: relatedOps.map((op) => ({
|
|
1674
1788
|
id: op.id,
|
|
1675
1789
|
timestamp: op.timestamp,
|
|
1676
1790
|
description: op.description,
|
|
@@ -1681,8 +1795,7 @@ export async function createJJ(options) {
|
|
|
1681
1795
|
/**
|
|
1682
1796
|
* Amend the working copy change
|
|
1683
1797
|
*
|
|
1684
|
-
* @param {
|
|
1685
|
-
* @param {string} [args.message] - New description message
|
|
1798
|
+
* @param {Record<string, any>} args - Arguments
|
|
1686
1799
|
* @returns {Promise<Object>} Updated change
|
|
1687
1800
|
*/
|
|
1688
1801
|
async amend(args = {}) {
|
|
@@ -1693,10 +1806,7 @@ export async function createJJ(options) {
|
|
|
1693
1806
|
/**
|
|
1694
1807
|
* Convenience: describe current change and create new one (jj commit)
|
|
1695
1808
|
*
|
|
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
|
|
1809
|
+
* @param {Record<string, any>} [args={}] - Arguments
|
|
1700
1810
|
* @returns {Promise<Object>} The newly created change
|
|
1701
1811
|
*/
|
|
1702
1812
|
async commit(args = {}) {
|
|
@@ -1704,19 +1814,18 @@ export async function createJJ(options) {
|
|
|
1704
1814
|
message: args.message,
|
|
1705
1815
|
author: args.author,
|
|
1706
1816
|
metadata: args.metadata,
|
|
1707
|
-
preserveSnapshot: args.preserveSnapshot
|
|
1817
|
+
preserveSnapshot: args.preserveSnapshot,
|
|
1708
1818
|
});
|
|
1709
1819
|
return await this.new({
|
|
1710
1820
|
message: args.nextMessage,
|
|
1711
|
-
preserveSnapshot: args.preserveSnapshot
|
|
1821
|
+
preserveSnapshot: args.preserveSnapshot,
|
|
1712
1822
|
});
|
|
1713
1823
|
},
|
|
1714
1824
|
|
|
1715
1825
|
/**
|
|
1716
1826
|
* Edit a specific change (make it the working copy)
|
|
1717
1827
|
*
|
|
1718
|
-
* @param {
|
|
1719
|
-
* @param {string} args.changeId - Change ID to edit
|
|
1828
|
+
* @param {Record<string, any>} args - Arguments
|
|
1720
1829
|
* @returns {Promise<Object>} Information about the edited change including changeId, description, and file count
|
|
1721
1830
|
*/
|
|
1722
1831
|
async edit(args) {
|
|
@@ -1736,7 +1845,7 @@ export async function createJJ(options) {
|
|
|
1736
1845
|
await userConfig.load();
|
|
1737
1846
|
|
|
1738
1847
|
const change = await graph.getChange(args.changeId);
|
|
1739
|
-
const user = userConfig.getUser();
|
|
1848
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
1740
1849
|
if (!change) {
|
|
1741
1850
|
throw new JJError('CHANGE_NOT_FOUND', `Change ${args.changeId} not found`, {
|
|
1742
1851
|
changeId: args.changeId,
|
|
@@ -1841,13 +1950,18 @@ export async function createJJ(options) {
|
|
|
1841
1950
|
});
|
|
1842
1951
|
|
|
1843
1952
|
// Dispatch workingcopy:switched event (informational)
|
|
1844
|
-
await dispatchEventAsync(
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1953
|
+
await dispatchEventAsync(
|
|
1954
|
+
jj,
|
|
1955
|
+
'workingcopy:switched',
|
|
1956
|
+
{
|
|
1957
|
+
operation: 'edit',
|
|
1958
|
+
fromChangeId: previousChangeId,
|
|
1959
|
+
toChangeId: args.changeId,
|
|
1960
|
+
change,
|
|
1961
|
+
timestamp: new Date().toISOString(),
|
|
1962
|
+
},
|
|
1963
|
+
{ cancelable: false }
|
|
1964
|
+
);
|
|
1851
1965
|
|
|
1852
1966
|
// Return information about the edited change
|
|
1853
1967
|
return {
|
|
@@ -1858,7 +1972,7 @@ export async function createJJ(options) {
|
|
|
1858
1972
|
timestamp: change.timestamp,
|
|
1859
1973
|
};
|
|
1860
1974
|
},
|
|
1861
|
-
|
|
1975
|
+
|
|
1862
1976
|
/**
|
|
1863
1977
|
* Undo last operation
|
|
1864
1978
|
*
|
|
@@ -1875,7 +1989,7 @@ export async function createJJ(options) {
|
|
|
1875
1989
|
// Get the current operation (the one being undone) to restore its pre-state
|
|
1876
1990
|
const currentOp = ops[ops.length - 1];
|
|
1877
1991
|
|
|
1878
|
-
const previousView = await oplog.undo();
|
|
1992
|
+
const previousView = /** @type {any} */ (await oplog.undo());
|
|
1879
1993
|
|
|
1880
1994
|
// Restore state from previous view
|
|
1881
1995
|
await workingCopy.setCurrentChange(previousView.workingCopy);
|
|
@@ -1913,18 +2027,25 @@ export async function createJJ(options) {
|
|
|
1913
2027
|
if (currentOp && currentOp.conflictsSnapshot) {
|
|
1914
2028
|
// Restore conflicts from the operation we're undoing (its pre-state)
|
|
1915
2029
|
conflicts.conflicts = new Map(Object.entries(currentOp.conflictsSnapshot.conflicts || {}));
|
|
1916
|
-
conflicts.fileConflicts = new Map(
|
|
2030
|
+
conflicts.fileConflicts = new Map(
|
|
2031
|
+
Object.entries(currentOp.conflictsSnapshot.fileConflicts || {})
|
|
2032
|
+
);
|
|
1917
2033
|
await conflicts.save();
|
|
1918
2034
|
} else {
|
|
1919
2035
|
// No conflicts snapshot - clear conflicts
|
|
1920
2036
|
await conflicts.clear();
|
|
1921
2037
|
}
|
|
1922
2038
|
|
|
1923
|
-
// Record undo operation
|
|
2039
|
+
// Record undo operation. Tag it with the view it undid so that redo()
|
|
2040
|
+
// can re-apply it (jj's progressive undo/redo, added in jj v0.33).
|
|
1924
2041
|
await oplog.recordOperation({
|
|
1925
2042
|
timestamp: new Date().toISOString(),
|
|
1926
2043
|
user: await getUserOplogInfo(),
|
|
1927
2044
|
description: 'undo operation',
|
|
2045
|
+
eventType: 'undo',
|
|
2046
|
+
undoneOpId: currentOp.id,
|
|
2047
|
+
undoneView: currentOp.view,
|
|
2048
|
+
undoneConflictsSnapshot: currentOp.conflictsSnapshot || null,
|
|
1928
2049
|
parents: [],
|
|
1929
2050
|
view: previousView,
|
|
1930
2051
|
});
|
|
@@ -1944,13 +2065,110 @@ export async function createJJ(options) {
|
|
|
1944
2065
|
};
|
|
1945
2066
|
},
|
|
1946
2067
|
|
|
2068
|
+
/**
|
|
2069
|
+
* Redo an operation previously undone with undo() (v1.5)
|
|
2070
|
+
*
|
|
2071
|
+
* Mirrors `jj redo` (jj v0.33): progressively re-applies the operations that
|
|
2072
|
+
* `undo()` reverted, most-recent-undo first. Each redo consumes exactly one
|
|
2073
|
+
* pending undo.
|
|
2074
|
+
*
|
|
2075
|
+
* @returns {Promise<Object>} Information about the redone operation
|
|
2076
|
+
*/
|
|
2077
|
+
async redo() {
|
|
2078
|
+
await oplog.load();
|
|
2079
|
+
const ops = await oplog.list();
|
|
2080
|
+
|
|
2081
|
+
// Operations already consumed by a prior redo.
|
|
2082
|
+
const alreadyRedone = new Set();
|
|
2083
|
+
for (const op of ops) {
|
|
2084
|
+
if (op.eventType === 'redo' && op.redoOf) {
|
|
2085
|
+
alreadyRedone.add(op.redoOf);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
|
|
2089
|
+
// Find the most recent undo operation that has not yet been redone.
|
|
2090
|
+
let undoOp = null;
|
|
2091
|
+
for (let i = ops.length - 1; i >= 0; i--) {
|
|
2092
|
+
if (ops[i].eventType === 'undo' && !alreadyRedone.has(ops[i].id)) {
|
|
2093
|
+
undoOp = ops[i];
|
|
2094
|
+
break;
|
|
2095
|
+
}
|
|
2096
|
+
}
|
|
2097
|
+
|
|
2098
|
+
if (!undoOp) {
|
|
2099
|
+
throw new JJError('NOTHING_TO_REDO', 'No undone operations to redo', {
|
|
2100
|
+
suggestion: 'redo() only reverses a preceding undo()',
|
|
2101
|
+
});
|
|
2102
|
+
}
|
|
2103
|
+
|
|
2104
|
+
const redoneView = undoOp.undoneView || {};
|
|
2105
|
+
|
|
2106
|
+
// Restore the working-copy pointer.
|
|
2107
|
+
if (redoneView.workingCopy) {
|
|
2108
|
+
await workingCopy.setCurrentChange(redoneView.workingCopy);
|
|
2109
|
+
}
|
|
2110
|
+
|
|
2111
|
+
// Restore the working-copy files captured in the undone view.
|
|
2112
|
+
if (redoneView.fileSnapshot) {
|
|
2113
|
+
for (const [filePath, content] of Object.entries(redoneView.fileSnapshot)) {
|
|
2114
|
+
try {
|
|
2115
|
+
const fullPath = path.join(dir, filePath);
|
|
2116
|
+
const pathParts = filePath.split('/');
|
|
2117
|
+
if (pathParts.length > 1) {
|
|
2118
|
+
const fullDirPath = path.join(dir, pathParts.slice(0, -1).join('/'));
|
|
2119
|
+
await fs.promises.mkdir(fullDirPath, { recursive: true });
|
|
2120
|
+
}
|
|
2121
|
+
await fs.promises.writeFile(fullPath, content, 'utf8');
|
|
2122
|
+
} catch (error) {
|
|
2123
|
+
throw new JJError(
|
|
2124
|
+
'REDO_FILE_RESTORE_FAILED',
|
|
2125
|
+
`Failed to restore file ${filePath} during redo: ${error.message}`,
|
|
2126
|
+
{ filePath, originalError: error.message }
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// Restore conflicts to the state that existed after the redone operation.
|
|
2133
|
+
await conflicts.load();
|
|
2134
|
+
if (undoOp.undoneConflictsSnapshot) {
|
|
2135
|
+
conflicts.conflicts = new Map(
|
|
2136
|
+
Object.entries(undoOp.undoneConflictsSnapshot.conflicts || {})
|
|
2137
|
+
);
|
|
2138
|
+
conflicts.fileConflicts = new Map(
|
|
2139
|
+
Object.entries(undoOp.undoneConflictsSnapshot.fileConflicts || {})
|
|
2140
|
+
);
|
|
2141
|
+
await conflicts.save();
|
|
2142
|
+
}
|
|
2143
|
+
|
|
2144
|
+
await oplog.recordOperation({
|
|
2145
|
+
timestamp: new Date().toISOString(),
|
|
2146
|
+
user: await getUserOplogInfo(),
|
|
2147
|
+
description: 'redo operation',
|
|
2148
|
+
eventType: 'redo',
|
|
2149
|
+
redoOf: undoOp.id,
|
|
2150
|
+
parents: [],
|
|
2151
|
+
view: redoneView,
|
|
2152
|
+
});
|
|
2153
|
+
|
|
2154
|
+
return {
|
|
2155
|
+
redoneOperation: {
|
|
2156
|
+
description: undoOp.description,
|
|
2157
|
+
undoneOpId: undoOp.undoneOpId,
|
|
2158
|
+
},
|
|
2159
|
+
restoredState: {
|
|
2160
|
+
workingCopy: redoneView.workingCopy,
|
|
2161
|
+
heads: redoneView.heads,
|
|
2162
|
+
fileCount: redoneView.fileSnapshot ? Object.keys(redoneView.fileSnapshot).length : 0,
|
|
2163
|
+
},
|
|
2164
|
+
};
|
|
2165
|
+
},
|
|
2166
|
+
|
|
1947
2167
|
/**
|
|
1948
2168
|
* View operation log (operation history)
|
|
1949
2169
|
*
|
|
1950
|
-
* @param {
|
|
1951
|
-
* @
|
|
1952
|
-
* @param {string} [opts.change] - Show operations for specific change
|
|
1953
|
-
* @returns {Promise<Array>} Array of operations
|
|
2170
|
+
* @param {Record<string, any>} [opts] - Options
|
|
2171
|
+
* @returns {Promise<Array<any>>} Array of operations
|
|
1954
2172
|
*/
|
|
1955
2173
|
async obslog(opts = {}) {
|
|
1956
2174
|
await oplog.load();
|
|
@@ -1961,11 +2179,11 @@ export async function createJJ(options) {
|
|
|
1961
2179
|
|
|
1962
2180
|
// Filter by change if specified
|
|
1963
2181
|
if (changeId) {
|
|
1964
|
-
operations = operations.filter(
|
|
1965
|
-
op
|
|
1966
|
-
op.view
|
|
1967
|
-
(op.view.
|
|
1968
|
-
|
|
2182
|
+
operations = operations.filter(
|
|
2183
|
+
(op) =>
|
|
2184
|
+
op.view &&
|
|
2185
|
+
(op.view.workingCopy === changeId ||
|
|
2186
|
+
(op.view.heads && op.view.heads.includes(changeId)))
|
|
1969
2187
|
);
|
|
1970
2188
|
}
|
|
1971
2189
|
|
|
@@ -1975,10 +2193,10 @@ export async function createJJ(options) {
|
|
|
1975
2193
|
}
|
|
1976
2194
|
|
|
1977
2195
|
// Transform to evolution events with operation field
|
|
1978
|
-
const events = operations.map(op => ({
|
|
2196
|
+
const events = operations.map((op) => ({
|
|
1979
2197
|
eventType: op.eventType || 'modify',
|
|
1980
2198
|
description: op.description,
|
|
1981
|
-
operation: op.id,
|
|
2199
|
+
operation: op.id, // Add operation ID
|
|
1982
2200
|
timestamp: op.timestamp,
|
|
1983
2201
|
user: op.user,
|
|
1984
2202
|
}));
|
|
@@ -1994,9 +2212,8 @@ export async function createJJ(options) {
|
|
|
1994
2212
|
/**
|
|
1995
2213
|
* List all operations
|
|
1996
2214
|
*
|
|
1997
|
-
* @param {
|
|
1998
|
-
* @
|
|
1999
|
-
* @returns {Promise<Array>} Array of operations
|
|
2215
|
+
* @param {Record<string, any>} [opts] - Options
|
|
2216
|
+
* @returns {Promise<Array<any>>} Array of operations
|
|
2000
2217
|
*/
|
|
2001
2218
|
async list(opts = {}) {
|
|
2002
2219
|
await oplog.load();
|
|
@@ -2012,20 +2229,19 @@ export async function createJJ(options) {
|
|
|
2012
2229
|
/**
|
|
2013
2230
|
* View repository at a specific operation (time travel)
|
|
2014
2231
|
*
|
|
2015
|
-
* @param {
|
|
2016
|
-
* @param {string} args.operation - Operation ID to view at
|
|
2232
|
+
* @param {Record<string, any>} args - Arguments
|
|
2017
2233
|
* @returns {Promise<Object>} Read-only repository view at that operation
|
|
2018
2234
|
*/
|
|
2019
2235
|
async at(args) {
|
|
2020
2236
|
if (!args || !args.operation) {
|
|
2021
2237
|
throw new JJError('INVALID_ARGUMENT', 'Missing operation ID', {
|
|
2022
|
-
suggestion: 'Provide { operation: operationId }'
|
|
2238
|
+
suggestion: 'Provide { operation: operationId }',
|
|
2023
2239
|
});
|
|
2024
2240
|
}
|
|
2025
2241
|
|
|
2026
2242
|
await oplog.load();
|
|
2027
2243
|
const ops = await oplog.list();
|
|
2028
|
-
const targetOp = ops.find(op => op.id === args.operation);
|
|
2244
|
+
const targetOp = ops.find((op) => op.id === args.operation);
|
|
2029
2245
|
|
|
2030
2246
|
if (!targetOp) {
|
|
2031
2247
|
throw new JJError('OPERATION_NOT_FOUND', `Operation ${args.operation} not found`);
|
|
@@ -2033,17 +2249,17 @@ export async function createJJ(options) {
|
|
|
2033
2249
|
|
|
2034
2250
|
// Create a read-only view at this operation
|
|
2035
2251
|
return {
|
|
2036
|
-
async log(
|
|
2252
|
+
async log(_logArgs = {}) {
|
|
2037
2253
|
// Get changes that existed at this operation
|
|
2038
2254
|
await graph.load();
|
|
2039
2255
|
const allChanges = await graph.getAllChanges();
|
|
2040
2256
|
|
|
2041
2257
|
// Filter to changes that existed before or at this operation
|
|
2042
2258
|
const opIndex = ops.indexOf(targetOp);
|
|
2043
|
-
const validChanges = allChanges.filter(change => {
|
|
2259
|
+
const validChanges = allChanges.filter((/** @type {any} */ change) => {
|
|
2044
2260
|
// 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)
|
|
2261
|
+
const changeOps = ops.filter(
|
|
2262
|
+
(op) => op.view && op.view.heads && op.view.heads.includes(change.changeId)
|
|
2047
2263
|
);
|
|
2048
2264
|
return changeOps.length > 0 && ops.indexOf(changeOps[0]) <= opIndex;
|
|
2049
2265
|
});
|
|
@@ -2067,8 +2283,7 @@ export async function createJJ(options) {
|
|
|
2067
2283
|
/**
|
|
2068
2284
|
* Show changes in a specific operation (matches `jj operation show`)
|
|
2069
2285
|
*
|
|
2070
|
-
* @param {
|
|
2071
|
-
* @param {string} args.operation - Operation ID
|
|
2286
|
+
* @param {Record<string, any>} args - Arguments
|
|
2072
2287
|
* @returns {Promise<Object>} Operation details with changes made
|
|
2073
2288
|
*/
|
|
2074
2289
|
async show(args) {
|
|
@@ -2080,7 +2295,7 @@ export async function createJJ(options) {
|
|
|
2080
2295
|
|
|
2081
2296
|
await oplog.load();
|
|
2082
2297
|
const ops = await oplog.list();
|
|
2083
|
-
const op = ops.find(o => o.id === args.operation);
|
|
2298
|
+
const op = ops.find((o) => o.id === args.operation);
|
|
2084
2299
|
|
|
2085
2300
|
if (!op) {
|
|
2086
2301
|
throw new JJError('OPERATION_NOT_FOUND', `Operation ${args.operation} not found`);
|
|
@@ -2112,9 +2327,7 @@ export async function createJJ(options) {
|
|
|
2112
2327
|
/**
|
|
2113
2328
|
* Compare repository state between two operations (matches `jj operation diff`)
|
|
2114
2329
|
*
|
|
2115
|
-
* @param {
|
|
2116
|
-
* @param {string} args.from - Source operation ID
|
|
2117
|
-
* @param {string} args.to - Target operation ID
|
|
2330
|
+
* @param {Record<string, any>} args - Arguments
|
|
2118
2331
|
* @returns {Promise<Object>} Differences between operations
|
|
2119
2332
|
*/
|
|
2120
2333
|
async diff(args) {
|
|
@@ -2126,8 +2339,8 @@ export async function createJJ(options) {
|
|
|
2126
2339
|
|
|
2127
2340
|
await oplog.load();
|
|
2128
2341
|
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);
|
|
2342
|
+
const fromOp = ops.find((o) => o.id === args.from);
|
|
2343
|
+
const toOp = ops.find((o) => o.id === args.to);
|
|
2131
2344
|
|
|
2132
2345
|
if (!fromOp) {
|
|
2133
2346
|
throw new JJError('OPERATION_NOT_FOUND', `Operation ${args.from} not found`);
|
|
@@ -2140,13 +2353,14 @@ export async function createJJ(options) {
|
|
|
2140
2353
|
const fromHeads = new Set(fromOp.view?.heads || []);
|
|
2141
2354
|
const toHeads = new Set(toOp.view?.heads || []);
|
|
2142
2355
|
|
|
2143
|
-
const addedHeads = [...toHeads].filter(h => !fromHeads.has(h));
|
|
2144
|
-
const removedHeads = [...fromHeads].filter(h => !toHeads.has(h));
|
|
2356
|
+
const addedHeads = [...toHeads].filter((h) => !fromHeads.has(h));
|
|
2357
|
+
const removedHeads = [...fromHeads].filter((h) => !toHeads.has(h));
|
|
2145
2358
|
|
|
2146
2359
|
// Compare bookmarks
|
|
2147
2360
|
const fromBookmarks = fromOp.view?.bookmarks || {};
|
|
2148
2361
|
const toBookmarks = toOp.view?.bookmarks || {};
|
|
2149
2362
|
|
|
2363
|
+
/** @type {Record<string, any>} */
|
|
2150
2364
|
const bookmarkChanges = {};
|
|
2151
2365
|
for (const name of new Set([...Object.keys(fromBookmarks), ...Object.keys(toBookmarks)])) {
|
|
2152
2366
|
if (fromBookmarks[name] !== toBookmarks[name]) {
|
|
@@ -2172,8 +2386,7 @@ export async function createJJ(options) {
|
|
|
2172
2386
|
/**
|
|
2173
2387
|
* Restore repository to a specific operation (matches `jj operation restore`)
|
|
2174
2388
|
*
|
|
2175
|
-
* @param {
|
|
2176
|
-
* @param {string} args.operation - Operation ID to restore to
|
|
2389
|
+
* @param {Record<string, any>} args - Arguments
|
|
2177
2390
|
* @returns {Promise<Object>} Restore result
|
|
2178
2391
|
*/
|
|
2179
2392
|
async restore(args) {
|
|
@@ -2185,7 +2398,7 @@ export async function createJJ(options) {
|
|
|
2185
2398
|
|
|
2186
2399
|
await oplog.load();
|
|
2187
2400
|
const ops = await oplog.list();
|
|
2188
|
-
const targetOp = ops.find(o => o.id === args.operation);
|
|
2401
|
+
const targetOp = ops.find((o) => o.id === args.operation);
|
|
2189
2402
|
|
|
2190
2403
|
if (!targetOp) {
|
|
2191
2404
|
throw new JJError('OPERATION_NOT_FOUND', `Operation ${args.operation} not found`);
|
|
@@ -2234,8 +2447,7 @@ export async function createJJ(options) {
|
|
|
2234
2447
|
* This creates a new operation that undoes the effects of the specified operation,
|
|
2235
2448
|
* without affecting any operations that came after it.
|
|
2236
2449
|
*
|
|
2237
|
-
* @param {
|
|
2238
|
-
* @param {string} args.operation - Operation ID to revert
|
|
2450
|
+
* @param {Record<string, any>} args - Arguments
|
|
2239
2451
|
* @returns {Promise<Object>} Revert result
|
|
2240
2452
|
*/
|
|
2241
2453
|
async revert(args) {
|
|
@@ -2247,7 +2459,7 @@ export async function createJJ(options) {
|
|
|
2247
2459
|
|
|
2248
2460
|
await oplog.load();
|
|
2249
2461
|
const ops = await oplog.list();
|
|
2250
|
-
const targetOp = ops.find(o => o.id === args.operation);
|
|
2462
|
+
const targetOp = ops.find((o) => o.id === args.operation);
|
|
2251
2463
|
|
|
2252
2464
|
if (!targetOp) {
|
|
2253
2465
|
throw new JJError('OPERATION_NOT_FOUND', `Operation ${args.operation} not found`);
|
|
@@ -2264,10 +2476,10 @@ export async function createJJ(options) {
|
|
|
2264
2476
|
const previousOp = ops[targetIndex - 1];
|
|
2265
2477
|
|
|
2266
2478
|
// Compute the inverse changes
|
|
2267
|
-
const inversChanges = {
|
|
2479
|
+
const inversChanges = /** @type {Record<string, any>} */ ({
|
|
2268
2480
|
bookmarks: {},
|
|
2269
2481
|
heads: [],
|
|
2270
|
-
};
|
|
2482
|
+
});
|
|
2271
2483
|
|
|
2272
2484
|
// Revert bookmark changes
|
|
2273
2485
|
const prevBookmarks = previousOp.view?.bookmarks || {};
|
|
@@ -2280,8 +2492,9 @@ export async function createJJ(options) {
|
|
|
2280
2492
|
await bookmarks.delete(name);
|
|
2281
2493
|
inversChanges.bookmarks[name] = { action: 'deleted' };
|
|
2282
2494
|
} else if (prevBookmarks[name] !== targetBookmarks[name]) {
|
|
2283
|
-
// Bookmark was moved - move it back
|
|
2284
|
-
|
|
2495
|
+
// Bookmark was moved - move it back (the bookmark still exists, so
|
|
2496
|
+
// use move() rather than set(), which is create-only).
|
|
2497
|
+
await bookmarks.move(name, prevBookmarks[name]);
|
|
2285
2498
|
inversChanges.bookmarks[name] = {
|
|
2286
2499
|
action: 'moved',
|
|
2287
2500
|
from: targetBookmarks[name],
|
|
@@ -2341,8 +2554,7 @@ export async function createJJ(options) {
|
|
|
2341
2554
|
* WARNING: This is a destructive operation that cannot be undone. Use with caution.
|
|
2342
2555
|
* In most cases, you should use undo(), restore(), or revert() instead.
|
|
2343
2556
|
*
|
|
2344
|
-
* @param {
|
|
2345
|
-
* @param {string} args.operation - Operation ID to abandon
|
|
2557
|
+
* @param {Record<string, any>} args - Arguments
|
|
2346
2558
|
* @returns {Promise<Object>} Abandon result with relinked children
|
|
2347
2559
|
*/
|
|
2348
2560
|
async abandon(args) {
|
|
@@ -2355,7 +2567,7 @@ export async function createJJ(options) {
|
|
|
2355
2567
|
await oplog.load();
|
|
2356
2568
|
|
|
2357
2569
|
// Call the OperationLog abandon method
|
|
2358
|
-
const result = await oplog.abandon(args.operation);
|
|
2570
|
+
const result = /** @type {any} */ (await oplog.abandon(args.operation));
|
|
2359
2571
|
|
|
2360
2572
|
// Record a new operation about the abandonment
|
|
2361
2573
|
// Note: This creates a meta-record that an operation was abandoned
|
|
@@ -2372,7 +2584,7 @@ export async function createJJ(options) {
|
|
|
2372
2584
|
},
|
|
2373
2585
|
metadata: {
|
|
2374
2586
|
abandonedOperation: args.operation,
|
|
2375
|
-
relinkedChildren: result.relinkedChildren.map(c => c.operationId),
|
|
2587
|
+
relinkedChildren: result.relinkedChildren.map((/** @type {any} */ c) => c.operationId),
|
|
2376
2588
|
},
|
|
2377
2589
|
});
|
|
2378
2590
|
|
|
@@ -2388,14 +2600,11 @@ export async function createJJ(options) {
|
|
|
2388
2600
|
// ========================================
|
|
2389
2601
|
// v0.2 FEATURES: History Editing
|
|
2390
2602
|
// ========================================
|
|
2391
|
-
|
|
2603
|
+
|
|
2392
2604
|
/**
|
|
2393
2605
|
* Squash source change into destination change
|
|
2394
2606
|
*
|
|
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)
|
|
2607
|
+
* @param {Record<string, any>} args - Arguments
|
|
2399
2608
|
*/
|
|
2400
2609
|
async squash(args = {}) {
|
|
2401
2610
|
// Check for interactive mode (not supported)
|
|
@@ -2426,7 +2635,11 @@ export async function createJJ(options) {
|
|
|
2426
2635
|
let destChangeId = dest;
|
|
2427
2636
|
if (!destChangeId && source === workingCopy.getCurrentChangeId()) {
|
|
2428
2637
|
const workingCopyChange = await graph.getChange(source);
|
|
2429
|
-
if (
|
|
2638
|
+
if (
|
|
2639
|
+
workingCopyChange &&
|
|
2640
|
+
workingCopyChange.parents &&
|
|
2641
|
+
workingCopyChange.parents.length > 0
|
|
2642
|
+
) {
|
|
2430
2643
|
destChangeId = workingCopyChange.parents[0];
|
|
2431
2644
|
} else {
|
|
2432
2645
|
throw new JJError('INVALID_ARGUMENTS', 'Cannot squash working copy: no parent found');
|
|
@@ -2473,7 +2686,7 @@ export async function createJJ(options) {
|
|
|
2473
2686
|
|
|
2474
2687
|
// If squashing the working copy, create a new empty working copy on top of dest
|
|
2475
2688
|
if (isSquashingWorkingCopy) {
|
|
2476
|
-
const user = userConfig.getUser();
|
|
2689
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
2477
2690
|
const newChangeId = generateChangeId();
|
|
2478
2691
|
const newChange = {
|
|
2479
2692
|
changeId: newChangeId,
|
|
@@ -2495,7 +2708,7 @@ export async function createJJ(options) {
|
|
|
2495
2708
|
fileSnapshot: {},
|
|
2496
2709
|
};
|
|
2497
2710
|
|
|
2498
|
-
await graph.addChange(newChange);
|
|
2711
|
+
await graph.addChange(newChange); // Middleware will sync to Git
|
|
2499
2712
|
await workingCopy.setCurrentChange(newChangeId);
|
|
2500
2713
|
newWorkingCopyId = newChangeId;
|
|
2501
2714
|
}
|
|
@@ -2515,27 +2728,29 @@ export async function createJJ(options) {
|
|
|
2515
2728
|
});
|
|
2516
2729
|
|
|
2517
2730
|
// Dispatch change:squashed event (informational)
|
|
2518
|
-
await dispatchEventAsync(
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2731
|
+
await dispatchEventAsync(
|
|
2732
|
+
jj,
|
|
2733
|
+
'change:squashed',
|
|
2734
|
+
{
|
|
2735
|
+
operation: 'squash',
|
|
2736
|
+
sourceChangeId: source,
|
|
2737
|
+
destChangeId: destChangeId,
|
|
2738
|
+
sourceChange,
|
|
2739
|
+
destChange,
|
|
2740
|
+
timestamp: new Date().toISOString(),
|
|
2741
|
+
},
|
|
2742
|
+
{ cancelable: false }
|
|
2743
|
+
);
|
|
2526
2744
|
|
|
2527
2745
|
return destChange;
|
|
2528
2746
|
},
|
|
2529
|
-
|
|
2747
|
+
|
|
2530
2748
|
/**
|
|
2531
2749
|
* Rebase a change to a new parent (matches `jj rebase`)
|
|
2532
2750
|
*
|
|
2533
2751
|
* This is the proper JJ CLI semantic for moving changes in history.
|
|
2534
2752
|
*
|
|
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
|
|
2753
|
+
* @param {Record<string, any>} args - Arguments
|
|
2539
2754
|
* @returns {Promise<Object>} Updated change object
|
|
2540
2755
|
*
|
|
2541
2756
|
* @example
|
|
@@ -2556,8 +2771,7 @@ export async function createJJ(options) {
|
|
|
2556
2771
|
/**
|
|
2557
2772
|
* Abandon a change
|
|
2558
2773
|
*
|
|
2559
|
-
* @param {
|
|
2560
|
-
* @param {string} [args.changeId] - Change ID to abandon (defaults to @ - working copy)
|
|
2774
|
+
* @param {Record<string, any>} [args] - Arguments
|
|
2561
2775
|
* @returns {Promise<Object>} The abandoned change object including changeId, description, and abandoned flag
|
|
2562
2776
|
*/
|
|
2563
2777
|
async abandon(args = {}) {
|
|
@@ -2569,7 +2783,7 @@ export async function createJJ(options) {
|
|
|
2569
2783
|
const changeId = args.changeId || workingCopy.getCurrentChangeId();
|
|
2570
2784
|
|
|
2571
2785
|
const change = await graph.getChange(changeId);
|
|
2572
|
-
const user = userConfig.getUser();
|
|
2786
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
2573
2787
|
|
|
2574
2788
|
if (!change) {
|
|
2575
2789
|
throw new JJError('CHANGE_NOT_FOUND', `Change ${changeId} not found`);
|
|
@@ -2601,12 +2815,17 @@ export async function createJJ(options) {
|
|
|
2601
2815
|
});
|
|
2602
2816
|
|
|
2603
2817
|
// Dispatch change:abandoned event (informational)
|
|
2604
|
-
await dispatchEventAsync(
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2609
|
-
|
|
2818
|
+
await dispatchEventAsync(
|
|
2819
|
+
jj,
|
|
2820
|
+
'change:abandoned',
|
|
2821
|
+
{
|
|
2822
|
+
operation: 'abandon',
|
|
2823
|
+
changeId: changeId,
|
|
2824
|
+
change,
|
|
2825
|
+
timestamp: new Date().toISOString(),
|
|
2826
|
+
},
|
|
2827
|
+
{ cancelable: false }
|
|
2828
|
+
);
|
|
2610
2829
|
|
|
2611
2830
|
return change;
|
|
2612
2831
|
},
|
|
@@ -2617,9 +2836,7 @@ export async function createJJ(options) {
|
|
|
2617
2836
|
* Similar to `git revert`, this creates a new change that undoes the changes made
|
|
2618
2837
|
* by the specified revision without modifying the original change.
|
|
2619
2838
|
*
|
|
2620
|
-
* @param {
|
|
2621
|
-
* @param {string} args.revision - Change ID to back out
|
|
2622
|
-
* @param {string} [args.message] - Custom description for the backout change
|
|
2839
|
+
* @param {Record<string, any>} args - Arguments
|
|
2623
2840
|
* @returns {Promise<Object>} The new backout change including changeId, description, and backedOut field
|
|
2624
2841
|
*/
|
|
2625
2842
|
async backout(args) {
|
|
@@ -2634,7 +2851,8 @@ export async function createJJ(options) {
|
|
|
2634
2851
|
|
|
2635
2852
|
if (!args || !args.revision) {
|
|
2636
2853
|
throw new JJError('INVALID_ARGUMENT', 'Missing revision argument', {
|
|
2637
|
-
suggestion:
|
|
2854
|
+
suggestion:
|
|
2855
|
+
'Provide the change ID to back out: { revision: "abc123..." } (or use change/changeId/target)',
|
|
2638
2856
|
});
|
|
2639
2857
|
}
|
|
2640
2858
|
|
|
@@ -2643,7 +2861,7 @@ export async function createJJ(options) {
|
|
|
2643
2861
|
await userConfig.load();
|
|
2644
2862
|
|
|
2645
2863
|
const originalChange = await graph.getChange(args.revision);
|
|
2646
|
-
const user = userConfig.getUser();
|
|
2864
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
2647
2865
|
|
|
2648
2866
|
if (!originalChange) {
|
|
2649
2867
|
throw new JJError('CHANGE_NOT_FOUND', `Change ${args.revision} not found`);
|
|
@@ -2713,6 +2931,112 @@ export async function createJJ(options) {
|
|
|
2713
2931
|
};
|
|
2714
2932
|
},
|
|
2715
2933
|
|
|
2934
|
+
/**
|
|
2935
|
+
* Revert a change by creating a new change that undoes it (v1.5)
|
|
2936
|
+
*
|
|
2937
|
+
* This is the canonical name in modern Jujutsu (`jj revert`), which fully
|
|
2938
|
+
* replaced `jj backout` in jj v0.35. `backout()` remains as a deprecated
|
|
2939
|
+
* alias for backward compatibility.
|
|
2940
|
+
*
|
|
2941
|
+
* @param {Record<string, any>} args - Revert arguments
|
|
2942
|
+
* @returns {Promise<Object>} Result including `revertedFrom`
|
|
2943
|
+
*/
|
|
2944
|
+
async revert(args) {
|
|
2945
|
+
const result = await jj.backout(args);
|
|
2946
|
+
return {
|
|
2947
|
+
changeId: result.changeId,
|
|
2948
|
+
description: result.description,
|
|
2949
|
+
revertedFrom: result.backedOut,
|
|
2950
|
+
// Keep the legacy field so existing callers keep working.
|
|
2951
|
+
backedOut: result.backedOut,
|
|
2952
|
+
fileSnapshot: result.fileSnapshot,
|
|
2953
|
+
};
|
|
2954
|
+
},
|
|
2955
|
+
|
|
2956
|
+
/**
|
|
2957
|
+
* Sign a change (matches `jj sign`, jj v0.27)
|
|
2958
|
+
*
|
|
2959
|
+
* A pure-JS library cannot verify GPG/SSH keys the way the jj CLI does, so
|
|
2960
|
+
* this records signature *metadata* on the change (backend/key/status). This
|
|
2961
|
+
* makes changes discoverable via the `signed()` revset and lets tooling
|
|
2962
|
+
* round-trip signature intent. Pass a real signer via `args.backend`/`args.key`.
|
|
2963
|
+
*
|
|
2964
|
+
* @param {Record<string, any>} [args] - Arguments
|
|
2965
|
+
* @returns {Promise<Object>} { changeId, signed, signature }
|
|
2966
|
+
*/
|
|
2967
|
+
async sign(args = {}) {
|
|
2968
|
+
await graph.load();
|
|
2969
|
+
await workingCopy.load();
|
|
2970
|
+
|
|
2971
|
+
const revision =
|
|
2972
|
+
args.revision || args.change || args.changeId || workingCopy.getCurrentChangeId();
|
|
2973
|
+
const change = await graph.getChange(revision);
|
|
2974
|
+
if (!change) {
|
|
2975
|
+
throw new JJError('CHANGE_NOT_FOUND', `Change ${revision} not found`);
|
|
2976
|
+
}
|
|
2977
|
+
|
|
2978
|
+
const signature = {
|
|
2979
|
+
status: 'good',
|
|
2980
|
+
backend: args.backend || 'none',
|
|
2981
|
+
key: args.key || null,
|
|
2982
|
+
timestamp: new Date().toISOString(),
|
|
2983
|
+
};
|
|
2984
|
+
|
|
2985
|
+
await graph.updateChange({ ...change, signed: true, signature });
|
|
2986
|
+
|
|
2987
|
+
await oplog.recordOperation({
|
|
2988
|
+
timestamp: new Date().toISOString(),
|
|
2989
|
+
user: await getUserOplogInfo(),
|
|
2990
|
+
description: `sign ${revision.slice(0, 8)}`,
|
|
2991
|
+
parents: [],
|
|
2992
|
+
view: {
|
|
2993
|
+
bookmarks: {},
|
|
2994
|
+
remoteBookmarks: {},
|
|
2995
|
+
heads: [revision],
|
|
2996
|
+
workingCopy: workingCopy.getCurrentChangeId(),
|
|
2997
|
+
},
|
|
2998
|
+
});
|
|
2999
|
+
|
|
3000
|
+
return { changeId: revision, signed: true, signature };
|
|
3001
|
+
},
|
|
3002
|
+
|
|
3003
|
+
/**
|
|
3004
|
+
* Remove a signature from a change (matches `jj unsign`, jj v0.27)
|
|
3005
|
+
*
|
|
3006
|
+
* @param {Record<string, any>} [args] - Arguments
|
|
3007
|
+
* @returns {Promise<Object>} { changeId, signed }
|
|
3008
|
+
*/
|
|
3009
|
+
async unsign(args = {}) {
|
|
3010
|
+
await graph.load();
|
|
3011
|
+
await workingCopy.load();
|
|
3012
|
+
|
|
3013
|
+
const revision =
|
|
3014
|
+
args.revision || args.change || args.changeId || workingCopy.getCurrentChangeId();
|
|
3015
|
+
const change = await graph.getChange(revision);
|
|
3016
|
+
if (!change) {
|
|
3017
|
+
throw new JJError('CHANGE_NOT_FOUND', `Change ${revision} not found`);
|
|
3018
|
+
}
|
|
3019
|
+
|
|
3020
|
+
const updated = { ...change, signed: false };
|
|
3021
|
+
delete updated.signature;
|
|
3022
|
+
await graph.updateChange(updated);
|
|
3023
|
+
|
|
3024
|
+
await oplog.recordOperation({
|
|
3025
|
+
timestamp: new Date().toISOString(),
|
|
3026
|
+
user: await getUserOplogInfo(),
|
|
3027
|
+
description: `unsign ${revision.slice(0, 8)}`,
|
|
3028
|
+
parents: [],
|
|
3029
|
+
view: {
|
|
3030
|
+
bookmarks: {},
|
|
3031
|
+
remoteBookmarks: {},
|
|
3032
|
+
heads: [revision],
|
|
3033
|
+
workingCopy: workingCopy.getCurrentChangeId(),
|
|
3034
|
+
},
|
|
3035
|
+
});
|
|
3036
|
+
|
|
3037
|
+
return { changeId: revision, signed: false };
|
|
3038
|
+
},
|
|
3039
|
+
|
|
2716
3040
|
/**
|
|
2717
3041
|
* Absorb working copy changes into ancestor changes
|
|
2718
3042
|
*
|
|
@@ -2727,8 +3051,7 @@ export async function createJJ(options) {
|
|
|
2727
3051
|
* 4. Update each ancestor with its modifications
|
|
2728
3052
|
* 5. Remove absorbed changes from working copy
|
|
2729
3053
|
*
|
|
2730
|
-
* @param {
|
|
2731
|
-
* @param {boolean} [args.dryRun] - Preview without making changes
|
|
3054
|
+
* @param {Record<string, any>} [args] - Optional arguments
|
|
2732
3055
|
* @returns {Promise<Object>} Result with affected changes
|
|
2733
3056
|
*/
|
|
2734
3057
|
async absorb(args = {}) {
|
|
@@ -2738,7 +3061,7 @@ export async function createJJ(options) {
|
|
|
2738
3061
|
|
|
2739
3062
|
const workingChangeId = workingCopy.getCurrentChangeId();
|
|
2740
3063
|
const workingChange = await graph.getChange(workingChangeId);
|
|
2741
|
-
const user = userConfig.getUser();
|
|
3064
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
2742
3065
|
|
|
2743
3066
|
if (!workingChange) {
|
|
2744
3067
|
// No working copy
|
|
@@ -2767,6 +3090,7 @@ export async function createJJ(options) {
|
|
|
2767
3090
|
await graph.updateChange(workingChange);
|
|
2768
3091
|
|
|
2769
3092
|
// Get parent snapshot to determine what's modified
|
|
3093
|
+
/** @type {Record<string, any>} */
|
|
2770
3094
|
let parentSnapshot = {};
|
|
2771
3095
|
if (workingChange.parents && workingChange.parents.length > 0) {
|
|
2772
3096
|
const parent = await graph.getChange(workingChange.parents[0]);
|
|
@@ -2776,6 +3100,7 @@ export async function createJJ(options) {
|
|
|
2776
3100
|
}
|
|
2777
3101
|
|
|
2778
3102
|
// Find modified files (existed in parent AND content is different)
|
|
3103
|
+
/** @type {Record<string, any>} */
|
|
2779
3104
|
const modifiedFiles = {};
|
|
2780
3105
|
for (const [filePath, content] of Object.entries(currentSnapshot)) {
|
|
2781
3106
|
// Only count as modified if file existed in parent (not a new file)
|
|
@@ -2792,6 +3117,7 @@ export async function createJJ(options) {
|
|
|
2792
3117
|
}
|
|
2793
3118
|
|
|
2794
3119
|
// Check if there are new files (not in parent)
|
|
3120
|
+
/** @type {Record<string, any>} */
|
|
2795
3121
|
const newFiles = {};
|
|
2796
3122
|
for (const [filePath, content] of Object.entries(currentSnapshot)) {
|
|
2797
3123
|
if (parentSnapshot[filePath] === undefined) {
|
|
@@ -2816,6 +3142,7 @@ export async function createJJ(options) {
|
|
|
2816
3142
|
|
|
2817
3143
|
// For each modified file, use line-level tracking to find which ancestor
|
|
2818
3144
|
// modified each line, then group modifications by ancestor
|
|
3145
|
+
/** @type {Record<string, any>} */
|
|
2819
3146
|
const ancestorModifications = {}; // ancestorId -> { filePath: content }
|
|
2820
3147
|
|
|
2821
3148
|
for (const filePath of Object.keys(modifiedFiles)) {
|
|
@@ -2943,9 +3270,8 @@ export async function createJJ(options) {
|
|
|
2943
3270
|
// Update working copy to reflect absorbed changes
|
|
2944
3271
|
// Working copy should now match parent's new state (after absorption) plus any new files
|
|
2945
3272
|
const updatedParent = await graph.getChange(workingChange.parents[0]);
|
|
2946
|
-
const newWorkingSnapshot =
|
|
2947
|
-
? { ...updatedParent.fileSnapshot }
|
|
2948
|
-
: {};
|
|
3273
|
+
const newWorkingSnapshot =
|
|
3274
|
+
updatedParent && updatedParent.fileSnapshot ? { ...updatedParent.fileSnapshot } : {};
|
|
2949
3275
|
|
|
2950
3276
|
// Add new files that weren't absorbed
|
|
2951
3277
|
for (const [filePath, content] of Object.entries(newFiles)) {
|
|
@@ -3049,7 +3375,7 @@ export async function createJJ(options) {
|
|
|
3049
3375
|
* @param {string} parentContent - Content from parent change
|
|
3050
3376
|
* @param {string} currentContent - Current working copy content
|
|
3051
3377
|
* @param {string[]} startingPoints - Change IDs to start search from (parent changeIds)
|
|
3052
|
-
* @returns {Promise<Map
|
|
3378
|
+
* @returns {Promise<Map<any, any>>} Map of ancestorId -> { lines: Map(lineNum -> content), originalContent }
|
|
3053
3379
|
*/
|
|
3054
3380
|
async _findLineAncestors(filePath, parentContent, currentContent, startingPoints) {
|
|
3055
3381
|
if (!startingPoints || startingPoints.length === 0) {
|
|
@@ -3203,7 +3529,11 @@ export async function createJJ(options) {
|
|
|
3203
3529
|
continue;
|
|
3204
3530
|
}
|
|
3205
3531
|
|
|
3206
|
-
if (
|
|
3532
|
+
if (
|
|
3533
|
+
change.parents &&
|
|
3534
|
+
change.parents.includes(currentId) &&
|
|
3535
|
+
!visited.has(change.changeId)
|
|
3536
|
+
) {
|
|
3207
3537
|
visited.add(change.changeId);
|
|
3208
3538
|
queue.push(change.changeId);
|
|
3209
3539
|
descendants.push(change.changeId);
|
|
@@ -3219,11 +3549,16 @@ export async function createJJ(options) {
|
|
|
3219
3549
|
*
|
|
3220
3550
|
* @private
|
|
3221
3551
|
* @param {string[]} descendantIds - List of descendant IDs to rebuild
|
|
3222
|
-
* @param {Map} originalStates - Map of changeId -> original state before absorb
|
|
3552
|
+
* @param {Map<any, any>} originalStates - Map of changeId -> original state before absorb
|
|
3223
3553
|
* @param {string} stopAtChangeId - Don't rebuild this change
|
|
3224
|
-
* @param {Set} directlyModified - Set of changeIds that were directly modified by absorb
|
|
3554
|
+
* @param {Set<any>} directlyModified - Set of changeIds that were directly modified by absorb
|
|
3225
3555
|
*/
|
|
3226
|
-
async _rebuildDescendantsWithStates(
|
|
3556
|
+
async _rebuildDescendantsWithStates(
|
|
3557
|
+
descendantIds,
|
|
3558
|
+
originalStates,
|
|
3559
|
+
stopAtChangeId,
|
|
3560
|
+
directlyModified = new Set()
|
|
3561
|
+
) {
|
|
3227
3562
|
// Sort descendants by depth (parents before children)
|
|
3228
3563
|
const sorted = await this._topologicalSort(descendantIds);
|
|
3229
3564
|
|
|
@@ -3269,6 +3604,7 @@ export async function createJJ(options) {
|
|
|
3269
3604
|
|
|
3270
3605
|
// For each file, track which lines the descendant modified from original parent
|
|
3271
3606
|
// then reapply those modifications to the updated parent
|
|
3607
|
+
/** @type {Record<string, any>} */
|
|
3272
3608
|
const newSnapshot = {};
|
|
3273
3609
|
|
|
3274
3610
|
const allFiles = new Set([
|
|
@@ -3290,9 +3626,15 @@ export async function createJJ(options) {
|
|
|
3290
3626
|
const resultLines = [...updatedParentLines];
|
|
3291
3627
|
|
|
3292
3628
|
// Find lines that descendant modified
|
|
3293
|
-
for (
|
|
3294
|
-
|
|
3295
|
-
|
|
3629
|
+
for (
|
|
3630
|
+
let i = 0;
|
|
3631
|
+
i < Math.max(originalDescendantLines.length, originalParentLines.length);
|
|
3632
|
+
i++
|
|
3633
|
+
) {
|
|
3634
|
+
const originalDescendantLine =
|
|
3635
|
+
i < originalDescendantLines.length ? originalDescendantLines[i] : undefined;
|
|
3636
|
+
const originalParentLine =
|
|
3637
|
+
i < originalParentLines.length ? originalParentLines[i] : undefined;
|
|
3296
3638
|
|
|
3297
3639
|
// If descendant modified this line from original parent, apply that modification
|
|
3298
3640
|
if (originalDescendantLine !== originalParentLine) {
|
|
@@ -3356,7 +3698,11 @@ export async function createJJ(options) {
|
|
|
3356
3698
|
continue;
|
|
3357
3699
|
}
|
|
3358
3700
|
|
|
3359
|
-
if (
|
|
3701
|
+
if (
|
|
3702
|
+
change.parents &&
|
|
3703
|
+
change.parents.includes(currentId) &&
|
|
3704
|
+
!visited.has(change.changeId)
|
|
3705
|
+
) {
|
|
3360
3706
|
visited.add(change.changeId);
|
|
3361
3707
|
queue.push(change.changeId);
|
|
3362
3708
|
descendants.push(change.changeId);
|
|
@@ -3394,6 +3740,7 @@ export async function createJJ(options) {
|
|
|
3394
3740
|
|
|
3395
3741
|
// For each file, track which lines the descendant modified from original parent
|
|
3396
3742
|
// then reapply those modifications to the updated parent
|
|
3743
|
+
/** @type {Record<string, any>} */
|
|
3397
3744
|
const newSnapshot = {};
|
|
3398
3745
|
|
|
3399
3746
|
const allFiles = new Set([
|
|
@@ -3417,7 +3764,8 @@ export async function createJJ(options) {
|
|
|
3417
3764
|
// Find lines that descendant modified
|
|
3418
3765
|
for (let i = 0; i < Math.max(descendantLines.length, originalParentLines.length); i++) {
|
|
3419
3766
|
const descendantLine = i < descendantLines.length ? descendantLines[i] : undefined;
|
|
3420
|
-
const originalParentLine =
|
|
3767
|
+
const originalParentLine =
|
|
3768
|
+
i < originalParentLines.length ? originalParentLines[i] : undefined;
|
|
3421
3769
|
|
|
3422
3770
|
// If descendant modified this line from original parent, apply that modification
|
|
3423
3771
|
if (descendantLine !== originalParentLine) {
|
|
@@ -3451,15 +3799,14 @@ export async function createJJ(options) {
|
|
|
3451
3799
|
* @returns {Promise<string[]>} Sorted change IDs
|
|
3452
3800
|
*/
|
|
3453
3801
|
async _topologicalSort(changeIds) {
|
|
3454
|
-
const changes = await Promise.all(
|
|
3455
|
-
changeIds.map(id => graph.getChange(id))
|
|
3456
|
-
);
|
|
3802
|
+
const changes = await Promise.all(changeIds.map((id) => graph.getChange(id)));
|
|
3457
3803
|
|
|
3804
|
+
/** @type {any[]} */
|
|
3458
3805
|
const sorted = [];
|
|
3459
3806
|
const visited = new Set();
|
|
3460
3807
|
const inProgress = new Set();
|
|
3461
3808
|
|
|
3462
|
-
const visit = async (change) => {
|
|
3809
|
+
const visit = async (/** @type {any} */ change) => {
|
|
3463
3810
|
if (!change || visited.has(change.changeId)) {
|
|
3464
3811
|
return;
|
|
3465
3812
|
}
|
|
@@ -3499,8 +3846,7 @@ export async function createJJ(options) {
|
|
|
3499
3846
|
* A parent is redundant if it's already an ancestor through another parent.
|
|
3500
3847
|
* This operation optimizes the graph structure without changing the content.
|
|
3501
3848
|
*
|
|
3502
|
-
* @param {
|
|
3503
|
-
* @param {string} args.revision - Change ID to simplify
|
|
3849
|
+
* @param {Record<string, any>} args - Arguments
|
|
3504
3850
|
* @returns {Promise<{changeId: string, simplified: boolean, removedParents?: string[]}>}
|
|
3505
3851
|
*/
|
|
3506
3852
|
async simplifyParents(args) {
|
|
@@ -3514,7 +3860,7 @@ export async function createJJ(options) {
|
|
|
3514
3860
|
await userConfig.load();
|
|
3515
3861
|
|
|
3516
3862
|
const change = await graph.getChange(args.revision);
|
|
3517
|
-
const user = userConfig.getUser();
|
|
3863
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
3518
3864
|
|
|
3519
3865
|
if (!change) {
|
|
3520
3866
|
throw new JJError('CHANGE_NOT_FOUND', `Change ${args.revision} not found`);
|
|
@@ -3530,6 +3876,7 @@ export async function createJJ(options) {
|
|
|
3530
3876
|
|
|
3531
3877
|
// Find redundant parents
|
|
3532
3878
|
// A parent P is redundant if there exists another parent Q such that P is an ancestor of Q
|
|
3879
|
+
/** @type {any[]} */
|
|
3533
3880
|
const redundantParents = [];
|
|
3534
3881
|
|
|
3535
3882
|
for (let i = 0; i < change.parents.length; i++) {
|
|
@@ -3543,7 +3890,7 @@ export async function createJJ(options) {
|
|
|
3543
3890
|
|
|
3544
3891
|
// Get all ancestors of parentQ
|
|
3545
3892
|
const ancestorsQ = await this.log({ revisions: [parentQ] });
|
|
3546
|
-
const ancestorIds = ancestorsQ.map(c => c.changeId);
|
|
3893
|
+
const ancestorIds = ancestorsQ.map((/** @type {any} */ c) => c.changeId);
|
|
3547
3894
|
|
|
3548
3895
|
// If parentP is in the ancestors of parentQ, then parentP is redundant
|
|
3549
3896
|
if (ancestorIds.includes(parentP)) {
|
|
@@ -3564,7 +3911,9 @@ export async function createJJ(options) {
|
|
|
3564
3911
|
}
|
|
3565
3912
|
|
|
3566
3913
|
// Remove redundant parents
|
|
3567
|
-
change.parents = change.parents.filter(
|
|
3914
|
+
change.parents = change.parents.filter(
|
|
3915
|
+
(/** @type {any} */ p) => !redundantParents.includes(p)
|
|
3916
|
+
);
|
|
3568
3917
|
await graph.updateChange(change);
|
|
3569
3918
|
|
|
3570
3919
|
// Record operation
|
|
@@ -3591,8 +3940,7 @@ export async function createJJ(options) {
|
|
|
3591
3940
|
/**
|
|
3592
3941
|
* Un-abandon a change (restore from abandoned state)
|
|
3593
3942
|
*
|
|
3594
|
-
* @param {
|
|
3595
|
-
* @param {string} args.changeId - Change ID to un-abandon
|
|
3943
|
+
* @param {Record<string, any>} args - Arguments
|
|
3596
3944
|
* @returns {Promise<Object>} The un-abandoned change object including changeId, description, and abandoned flag (now false)
|
|
3597
3945
|
*/
|
|
3598
3946
|
async unabandon(args) {
|
|
@@ -3617,7 +3965,7 @@ export async function createJJ(options) {
|
|
|
3617
3965
|
await userConfig.load();
|
|
3618
3966
|
|
|
3619
3967
|
const change = await graph.getChange(args.changeId);
|
|
3620
|
-
const user = userConfig.getUser();
|
|
3968
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
3621
3969
|
|
|
3622
3970
|
if (!change) {
|
|
3623
3971
|
throw new JJError('CHANGE_NOT_FOUND', `Change ${args.changeId} not found`);
|
|
@@ -3649,24 +3997,25 @@ export async function createJJ(options) {
|
|
|
3649
3997
|
});
|
|
3650
3998
|
|
|
3651
3999
|
// Dispatch change:unabandoned event (informational)
|
|
3652
|
-
await dispatchEventAsync(
|
|
3653
|
-
|
|
3654
|
-
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
4000
|
+
await dispatchEventAsync(
|
|
4001
|
+
jj,
|
|
4002
|
+
'change:unabandoned',
|
|
4003
|
+
{
|
|
4004
|
+
operation: 'unabandon',
|
|
4005
|
+
changeId: args.changeId,
|
|
4006
|
+
change,
|
|
4007
|
+
timestamp: new Date().toISOString(),
|
|
4008
|
+
},
|
|
4009
|
+
{ cancelable: false }
|
|
4010
|
+
);
|
|
3658
4011
|
|
|
3659
4012
|
return change;
|
|
3660
4013
|
},
|
|
3661
|
-
|
|
4014
|
+
|
|
3662
4015
|
/**
|
|
3663
4016
|
* Split a change into multiple changes (simplified v0.2 implementation)
|
|
3664
4017
|
*
|
|
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)
|
|
4018
|
+
* @param {Record<string, any>} args - Arguments
|
|
3670
4019
|
*/
|
|
3671
4020
|
async split(args) {
|
|
3672
4021
|
// Normalize: accept common parameter aliases
|
|
@@ -3746,7 +4095,7 @@ export async function createJJ(options) {
|
|
|
3746
4095
|
|
|
3747
4096
|
return { original: originalChange, new: newChange };
|
|
3748
4097
|
},
|
|
3749
|
-
|
|
4098
|
+
|
|
3750
4099
|
// ========================================
|
|
3751
4100
|
// v0.3 FEATURES: Git Integration
|
|
3752
4101
|
// ========================================
|
|
@@ -3759,18 +4108,13 @@ export async function createJJ(options) {
|
|
|
3759
4108
|
* Initialize a new Git-backed JJ repository
|
|
3760
4109
|
* Matches `jj git init` CLI command
|
|
3761
4110
|
*
|
|
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')
|
|
4111
|
+
* @param {Record<string, any>} opts - Initialization options
|
|
3766
4112
|
*/
|
|
3767
4113
|
async init(opts = {}) {
|
|
3768
4114
|
if (!gitBackend) {
|
|
3769
|
-
throw new JJError(
|
|
3770
|
-
'
|
|
3771
|
-
|
|
3772
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
3773
|
-
);
|
|
4115
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4116
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4117
|
+
});
|
|
3774
4118
|
}
|
|
3775
4119
|
|
|
3776
4120
|
// Initialize Git backend
|
|
@@ -3795,7 +4139,7 @@ export async function createJJ(options) {
|
|
|
3795
4139
|
|
|
3796
4140
|
// Create root change
|
|
3797
4141
|
const rootChangeId = generateChangeId();
|
|
3798
|
-
const user = userConfig.getUser();
|
|
4142
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
3799
4143
|
const rootChange = {
|
|
3800
4144
|
changeId: rootChangeId,
|
|
3801
4145
|
commitId: '0000000000000000000000000000000000000000',
|
|
@@ -3840,32 +4184,22 @@ export async function createJJ(options) {
|
|
|
3840
4184
|
/**
|
|
3841
4185
|
* Fetch changes from remote Git repository
|
|
3842
4186
|
*
|
|
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
|
|
4187
|
+
* @param {Record<string, any>} args - Fetch arguments
|
|
3852
4188
|
*/
|
|
3853
4189
|
async fetch(args) {
|
|
3854
4190
|
if (!gitBackend) {
|
|
3855
|
-
throw new JJError(
|
|
3856
|
-
'
|
|
3857
|
-
|
|
3858
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
3859
|
-
);
|
|
4191
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4192
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4193
|
+
});
|
|
3860
4194
|
}
|
|
3861
4195
|
|
|
3862
4196
|
const result = await gitBackend.fetch({
|
|
3863
4197
|
remote: args.remote,
|
|
3864
4198
|
refs: args.refs,
|
|
3865
|
-
depth: args.depth,
|
|
3866
|
-
relative: args.relative,
|
|
4199
|
+
depth: args.depth, // v0.4: Shallow clone support
|
|
4200
|
+
relative: args.relative, // v0.4: Relative depth
|
|
3867
4201
|
singleBranch: args.singleBranch, // v0.4: Single branch only
|
|
3868
|
-
noTags: args.noTags,
|
|
4202
|
+
noTags: args.noTags, // v0.4: Skip tags
|
|
3869
4203
|
onProgress: args.onProgress,
|
|
3870
4204
|
onAuth: args.onAuth,
|
|
3871
4205
|
});
|
|
@@ -3890,20 +4224,13 @@ export async function createJJ(options) {
|
|
|
3890
4224
|
/**
|
|
3891
4225
|
* Push changes to remote Git repository
|
|
3892
4226
|
*
|
|
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
|
|
4227
|
+
* @param {Record<string, any>} args - Push arguments
|
|
3899
4228
|
*/
|
|
3900
4229
|
async push(args) {
|
|
3901
4230
|
if (!gitBackend) {
|
|
3902
|
-
throw new JJError(
|
|
3903
|
-
'
|
|
3904
|
-
|
|
3905
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
3906
|
-
);
|
|
4231
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4232
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4233
|
+
});
|
|
3907
4234
|
}
|
|
3908
4235
|
|
|
3909
4236
|
const result = await gitBackend.push({
|
|
@@ -3934,24 +4261,14 @@ export async function createJJ(options) {
|
|
|
3934
4261
|
/**
|
|
3935
4262
|
* Clone a Git repository (matches `jj git clone`)
|
|
3936
4263
|
*
|
|
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
|
|
4264
|
+
* @param {Record<string, any>} args - Clone arguments
|
|
3946
4265
|
* @returns {Promise<Object>} Clone result with directory path
|
|
3947
4266
|
*/
|
|
3948
4267
|
async clone(args) {
|
|
3949
4268
|
if (!gitBackend) {
|
|
3950
|
-
throw new JJError(
|
|
3951
|
-
'
|
|
3952
|
-
|
|
3953
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
3954
|
-
);
|
|
4269
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4270
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4271
|
+
});
|
|
3955
4272
|
}
|
|
3956
4273
|
|
|
3957
4274
|
if (!args || !args.url) {
|
|
@@ -3968,7 +4285,12 @@ export async function createJJ(options) {
|
|
|
3968
4285
|
|
|
3969
4286
|
// Determine clone directory
|
|
3970
4287
|
const git = (await import('isomorphic-git')).default;
|
|
3971
|
-
const cloneDir =
|
|
4288
|
+
const cloneDir =
|
|
4289
|
+
args.dir ||
|
|
4290
|
+
args.url
|
|
4291
|
+
.split('/')
|
|
4292
|
+
.pop()
|
|
4293
|
+
.replace(/\.git$/, '');
|
|
3972
4294
|
const fullCloneDir = path.join(dir, cloneDir);
|
|
3973
4295
|
|
|
3974
4296
|
// Clone using isomorphic-git
|
|
@@ -3986,7 +4308,9 @@ export async function createJJ(options) {
|
|
|
3986
4308
|
});
|
|
3987
4309
|
|
|
3988
4310
|
// Initialize JJ repository structure in cloned directory
|
|
3989
|
-
const clonedBackend =
|
|
4311
|
+
const clonedBackend = /** @type {any} */ (
|
|
4312
|
+
new IsomorphicGitBackend({ fs, http, dir: fullCloneDir })
|
|
4313
|
+
);
|
|
3990
4314
|
await clonedBackend._createJJRepoStructure();
|
|
3991
4315
|
|
|
3992
4316
|
// Record operation
|
|
@@ -4017,11 +4341,9 @@ export async function createJJ(options) {
|
|
|
4017
4341
|
*/
|
|
4018
4342
|
async import() {
|
|
4019
4343
|
if (!gitBackend) {
|
|
4020
|
-
throw new JJError(
|
|
4021
|
-
'
|
|
4022
|
-
|
|
4023
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
4024
|
-
);
|
|
4344
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4345
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4346
|
+
});
|
|
4025
4347
|
}
|
|
4026
4348
|
|
|
4027
4349
|
await graph.load();
|
|
@@ -4039,14 +4361,18 @@ export async function createJJ(options) {
|
|
|
4039
4361
|
if (!change) {
|
|
4040
4362
|
// Create a new change for this commit
|
|
4041
4363
|
const changeId = generateChangeId();
|
|
4042
|
-
const user = userConfig.getUser();
|
|
4364
|
+
const user = /** @type {any} */ (userConfig.getUser());
|
|
4043
4365
|
change = {
|
|
4044
4366
|
changeId,
|
|
4045
4367
|
commitId: ref.oid,
|
|
4046
4368
|
parents: [],
|
|
4047
4369
|
tree: ref.oid,
|
|
4048
4370
|
author: { name: user.name, email: user.email, timestamp: new Date().toISOString() },
|
|
4049
|
-
committer: {
|
|
4371
|
+
committer: {
|
|
4372
|
+
name: user.name,
|
|
4373
|
+
email: user.email,
|
|
4374
|
+
timestamp: new Date().toISOString(),
|
|
4375
|
+
},
|
|
4050
4376
|
description: `Imported from Git ref ${ref.name}`,
|
|
4051
4377
|
timestamp: new Date().toISOString(),
|
|
4052
4378
|
};
|
|
@@ -4090,11 +4416,9 @@ export async function createJJ(options) {
|
|
|
4090
4416
|
*/
|
|
4091
4417
|
async export() {
|
|
4092
4418
|
if (!gitBackend) {
|
|
4093
|
-
throw new JJError(
|
|
4094
|
-
'
|
|
4095
|
-
|
|
4096
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
4097
|
-
);
|
|
4419
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4420
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4421
|
+
});
|
|
4098
4422
|
}
|
|
4099
4423
|
|
|
4100
4424
|
await graph.load();
|
|
@@ -4139,37 +4463,31 @@ export async function createJJ(options) {
|
|
|
4139
4463
|
/**
|
|
4140
4464
|
* List Git remotes (matches `jj git remote list`)
|
|
4141
4465
|
*
|
|
4142
|
-
* @returns {Promise<Array
|
|
4466
|
+
* @returns {Promise<Array<any>>} List of remotes with names and URLs
|
|
4143
4467
|
*/
|
|
4144
4468
|
async list() {
|
|
4145
4469
|
if (!gitBackend) {
|
|
4146
|
-
throw new JJError(
|
|
4147
|
-
'
|
|
4148
|
-
|
|
4149
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
4150
|
-
);
|
|
4470
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4471
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4472
|
+
});
|
|
4151
4473
|
}
|
|
4152
4474
|
|
|
4153
4475
|
const git = (await import('isomorphic-git')).default;
|
|
4154
4476
|
const remotes = await git.listRemotes({ fs, dir });
|
|
4155
|
-
return remotes.map(r => ({ name: r.remote, url: r.url }));
|
|
4477
|
+
return remotes.map((r) => ({ name: r.remote, url: r.url }));
|
|
4156
4478
|
},
|
|
4157
4479
|
|
|
4158
4480
|
/**
|
|
4159
4481
|
* Add a Git remote (matches `jj git remote add`)
|
|
4160
4482
|
*
|
|
4161
|
-
* @param {
|
|
4162
|
-
* @param {string} args.name - Remote name
|
|
4163
|
-
* @param {string} args.url - Remote URL
|
|
4483
|
+
* @param {Record<string, any>} args - Remote arguments
|
|
4164
4484
|
* @returns {Promise<Object>} Added remote info
|
|
4165
4485
|
*/
|
|
4166
4486
|
async add(args) {
|
|
4167
4487
|
if (!gitBackend) {
|
|
4168
|
-
throw new JJError(
|
|
4169
|
-
'
|
|
4170
|
-
|
|
4171
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
4172
|
-
);
|
|
4488
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4489
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4490
|
+
});
|
|
4173
4491
|
}
|
|
4174
4492
|
|
|
4175
4493
|
if (!args || !args.name || !args.url) {
|
|
@@ -4206,17 +4524,14 @@ export async function createJJ(options) {
|
|
|
4206
4524
|
/**
|
|
4207
4525
|
* Remove a Git remote (matches `jj git remote remove`)
|
|
4208
4526
|
*
|
|
4209
|
-
* @param {
|
|
4210
|
-
* @param {string} args.name - Remote name to remove
|
|
4527
|
+
* @param {Record<string, any>} args - Remote arguments
|
|
4211
4528
|
* @returns {Promise<Object>} Removal result
|
|
4212
4529
|
*/
|
|
4213
4530
|
async remove(args) {
|
|
4214
4531
|
if (!gitBackend) {
|
|
4215
|
-
throw new JJError(
|
|
4216
|
-
'
|
|
4217
|
-
|
|
4218
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
4219
|
-
);
|
|
4532
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4533
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4534
|
+
});
|
|
4220
4535
|
}
|
|
4221
4536
|
|
|
4222
4537
|
if (!args || !args.name) {
|
|
@@ -4252,18 +4567,14 @@ export async function createJJ(options) {
|
|
|
4252
4567
|
/**
|
|
4253
4568
|
* Rename a Git remote (matches `jj git remote rename`)
|
|
4254
4569
|
*
|
|
4255
|
-
* @param {
|
|
4256
|
-
* @param {string} args.oldName - Current remote name
|
|
4257
|
-
* @param {string} args.newName - New remote name
|
|
4570
|
+
* @param {Record<string, any>} args - Remote arguments
|
|
4258
4571
|
* @returns {Promise<Object>} Rename result
|
|
4259
4572
|
*/
|
|
4260
4573
|
async rename(args) {
|
|
4261
4574
|
if (!gitBackend) {
|
|
4262
|
-
throw new JJError(
|
|
4263
|
-
'
|
|
4264
|
-
|
|
4265
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
4266
|
-
);
|
|
4575
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4576
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4577
|
+
});
|
|
4267
4578
|
}
|
|
4268
4579
|
|
|
4269
4580
|
if (!args || !args.oldName || !args.newName) {
|
|
@@ -4276,7 +4587,7 @@ export async function createJJ(options) {
|
|
|
4276
4587
|
|
|
4277
4588
|
// Get current URL
|
|
4278
4589
|
const remotes = await git.listRemotes({ fs, dir });
|
|
4279
|
-
const remote = remotes.find(r => r.remote === args.oldName);
|
|
4590
|
+
const remote = remotes.find((r) => r.remote === args.oldName);
|
|
4280
4591
|
if (!remote) {
|
|
4281
4592
|
throw new JJError('NOT_FOUND', `Remote ${args.oldName} not found`);
|
|
4282
4593
|
}
|
|
@@ -4305,18 +4616,14 @@ export async function createJJ(options) {
|
|
|
4305
4616
|
/**
|
|
4306
4617
|
* Set URL for a Git remote (matches `jj git remote set-url`)
|
|
4307
4618
|
*
|
|
4308
|
-
* @param {
|
|
4309
|
-
* @param {string} args.name - Remote name
|
|
4310
|
-
* @param {string} args.url - New URL
|
|
4619
|
+
* @param {Record<string, any>} args - Remote arguments
|
|
4311
4620
|
* @returns {Promise<Object>} Updated remote info
|
|
4312
4621
|
*/
|
|
4313
4622
|
async setUrl(args) {
|
|
4314
4623
|
if (!gitBackend) {
|
|
4315
|
-
throw new JJError(
|
|
4316
|
-
'
|
|
4317
|
-
|
|
4318
|
-
{ suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })' }
|
|
4319
|
-
);
|
|
4624
|
+
throw new JJError('BACKEND_NOT_AVAILABLE', 'Git backend not configured', {
|
|
4625
|
+
suggestion: 'Provide git instance: createJJ({ fs, dir, git, http })',
|
|
4626
|
+
});
|
|
4320
4627
|
}
|
|
4321
4628
|
|
|
4322
4629
|
if (!args || !args.name || !args.url) {
|
|
@@ -4329,7 +4636,7 @@ export async function createJJ(options) {
|
|
|
4329
4636
|
|
|
4330
4637
|
// Check if remote exists
|
|
4331
4638
|
const remotes = await git.listRemotes({ fs, dir });
|
|
4332
|
-
const remote = remotes.find(r => r.remote === args.name);
|
|
4639
|
+
const remote = remotes.find((r) => r.remote === args.name);
|
|
4333
4640
|
if (!remote) {
|
|
4334
4641
|
throw new JJError('NOT_FOUND', `Remote ${args.name} not found`);
|
|
4335
4642
|
}
|
|
@@ -4383,8 +4690,7 @@ export async function createJJ(options) {
|
|
|
4383
4690
|
/**
|
|
4384
4691
|
* Merge changes from another change, detecting conflicts
|
|
4385
4692
|
*
|
|
4386
|
-
* @param {
|
|
4387
|
-
* @param {string} args.source - Source change ID to merge
|
|
4693
|
+
* @param {Record<string, any>} args - Merge arguments
|
|
4388
4694
|
* @returns {Promise<Object>} Merge result with conflicts
|
|
4389
4695
|
*/
|
|
4390
4696
|
async merge(args) {
|
|
@@ -4402,7 +4708,7 @@ export async function createJJ(options) {
|
|
|
4402
4708
|
};
|
|
4403
4709
|
|
|
4404
4710
|
const currentChangeId = workingCopy.getCurrentChangeId();
|
|
4405
|
-
const
|
|
4711
|
+
const _currentChange = await graph.getChange(currentChangeId);
|
|
4406
4712
|
const sourceChange = await graph.getChange(args.source);
|
|
4407
4713
|
|
|
4408
4714
|
if (!sourceChange) {
|
|
@@ -4467,9 +4773,9 @@ export async function createJJ(options) {
|
|
|
4467
4773
|
baseFiles,
|
|
4468
4774
|
leftFiles,
|
|
4469
4775
|
rightFiles,
|
|
4470
|
-
drivers: args.drivers || {},
|
|
4471
|
-
workingCopyDir: args.dryRun ? null : dir,
|
|
4472
|
-
baseChange: baseChangeId,
|
|
4776
|
+
drivers: args.drivers || {}, // v0.5: custom merge drivers
|
|
4777
|
+
workingCopyDir: /** @type {any} */ (args.dryRun ? null : dir), // v0.5: skip file writes in dry-run
|
|
4778
|
+
baseChange: baseChangeId, // v0.5: metadata for drivers
|
|
4473
4779
|
leftChange: currentChangeId,
|
|
4474
4780
|
rightChange: args.source,
|
|
4475
4781
|
});
|
|
@@ -4522,8 +4828,7 @@ export async function createJJ(options) {
|
|
|
4522
4828
|
/**
|
|
4523
4829
|
* List active (unresolved) conflicts
|
|
4524
4830
|
*
|
|
4525
|
-
* @param {
|
|
4526
|
-
* @param {boolean} [opts.includeResolved=false] - Include resolved conflicts
|
|
4831
|
+
* @param {Record<string, any>} opts - List options
|
|
4527
4832
|
*/
|
|
4528
4833
|
async list(opts = {}) {
|
|
4529
4834
|
await conflicts.load();
|
|
@@ -4535,11 +4840,7 @@ export async function createJJ(options) {
|
|
|
4535
4840
|
/**
|
|
4536
4841
|
* Resolve a conflict (v0.5 enhanced)
|
|
4537
4842
|
*
|
|
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')
|
|
4843
|
+
* @param {Record<string, any>} args - Resolution arguments
|
|
4543
4844
|
*/
|
|
4544
4845
|
async resolve(args) {
|
|
4545
4846
|
if (!args || !args.conflictId) {
|
|
@@ -4569,12 +4870,12 @@ export async function createJJ(options) {
|
|
|
4569
4870
|
|
|
4570
4871
|
const driverResult = await mergeDrivers.executeDriver(
|
|
4571
4872
|
driver,
|
|
4572
|
-
{
|
|
4873
|
+
/** @type {any} */ ({
|
|
4573
4874
|
path: conflict.path,
|
|
4574
4875
|
base: conflict.sides.base,
|
|
4575
4876
|
ours: conflict.sides.left,
|
|
4576
4877
|
theirs: conflict.sides.right,
|
|
4577
|
-
},
|
|
4878
|
+
}),
|
|
4578
4879
|
mergeDrivers.isBinaryFile(conflict.path, conflict.sides.left)
|
|
4579
4880
|
);
|
|
4580
4881
|
|
|
@@ -4603,10 +4904,7 @@ export async function createJJ(options) {
|
|
|
4603
4904
|
/**
|
|
4604
4905
|
* Resolve all conflicts (v0.5)
|
|
4605
4906
|
*
|
|
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
|
|
4907
|
+
* @param {Record<string, any>} args - Resolution arguments
|
|
4610
4908
|
*/
|
|
4611
4909
|
async resolveAll(args) {
|
|
4612
4910
|
if (!args || !args.strategy) {
|
|
@@ -4620,7 +4918,7 @@ export async function createJJ(options) {
|
|
|
4620
4918
|
let toResolve = allConflicts;
|
|
4621
4919
|
if (args.filter && args.filter.path) {
|
|
4622
4920
|
const pattern = args.filter.path;
|
|
4623
|
-
toResolve = allConflicts.filter(c => {
|
|
4921
|
+
toResolve = allConflicts.filter((c) => {
|
|
4624
4922
|
// Simple pattern matching - exact match or glob
|
|
4625
4923
|
if (pattern.includes('*')) {
|
|
4626
4924
|
const regex = new RegExp('^' + pattern.replace(/\*/g, '.*') + '$');
|
|
@@ -4655,8 +4953,7 @@ export async function createJJ(options) {
|
|
|
4655
4953
|
/**
|
|
4656
4954
|
* Get conflict markers for a conflict (v0.5)
|
|
4657
4955
|
*
|
|
4658
|
-
* @param {
|
|
4659
|
-
* @param {string} args.conflictId - Conflict ID
|
|
4956
|
+
* @param {Record<string, any>} args - Arguments
|
|
4660
4957
|
* @returns {Promise<string>} Conflict markers in standard format
|
|
4661
4958
|
*/
|
|
4662
4959
|
async markers(args) {
|
|
@@ -4681,7 +4978,7 @@ export async function createJJ(options) {
|
|
|
4681
4978
|
/**
|
|
4682
4979
|
* Mark conflict as resolved
|
|
4683
4980
|
*/
|
|
4684
|
-
async markResolved(args) {
|
|
4981
|
+
async markResolved(/** @type {Record<string, any>} */ args) {
|
|
4685
4982
|
if (!args || !args.conflictId) {
|
|
4686
4983
|
throw new JJError('INVALID_ARGUMENT', 'Missing conflictId');
|
|
4687
4984
|
}
|
|
@@ -4702,19 +4999,15 @@ export async function createJJ(options) {
|
|
|
4702
4999
|
*
|
|
4703
5000
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
4704
5001
|
*/
|
|
4705
|
-
async resolve(args = {}) {
|
|
5002
|
+
async resolve(/** @type {Record<string, any>} */ args = {}) {
|
|
4706
5003
|
// Check for external merge tool usage
|
|
4707
5004
|
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
|
-
);
|
|
5005
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'External merge tools not supported', {
|
|
5006
|
+
feature: 'resolve --tool',
|
|
5007
|
+
reason: 'External merge tools require system integration not available in library API',
|
|
5008
|
+
alternative: 'Use jj.conflicts.resolve() with programmatic resolution',
|
|
5009
|
+
suggestion: 'For conflict resolution, use the programmatic API or resolve files manually',
|
|
5010
|
+
});
|
|
4718
5011
|
}
|
|
4719
5012
|
|
|
4720
5013
|
throw new JJError(
|
|
@@ -4743,10 +5036,7 @@ export async function createJJ(options) {
|
|
|
4743
5036
|
/**
|
|
4744
5037
|
* Show file content (matches `jj file show`)
|
|
4745
5038
|
*
|
|
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')
|
|
5039
|
+
* @param {Record<string, any>} args - Arguments
|
|
4750
5040
|
* @returns {Promise<string|Uint8Array>} File contents
|
|
4751
5041
|
*/
|
|
4752
5042
|
async show(args) {
|
|
@@ -4756,20 +5046,80 @@ export async function createJJ(options) {
|
|
|
4756
5046
|
/**
|
|
4757
5047
|
* List files (matches `jj file list`)
|
|
4758
5048
|
*
|
|
4759
|
-
* @param {
|
|
4760
|
-
* @param {string} [args.changeId] - Change ID (defaults to working copy)
|
|
5049
|
+
* @param {Record<string, any>} [args={}] - Arguments
|
|
4761
5050
|
* @returns {Promise<Array<string>>} Array of file paths
|
|
4762
5051
|
*/
|
|
4763
5052
|
async list(args = {}) {
|
|
4764
5053
|
return await jj.listFiles(args);
|
|
4765
5054
|
},
|
|
4766
5055
|
|
|
5056
|
+
/**
|
|
5057
|
+
* Search file contents (matches `jj file search`, jj v0.37).
|
|
5058
|
+
*
|
|
5059
|
+
* Searches the tracked files of a change (defaults to the working copy)
|
|
5060
|
+
* for lines matching a pattern. Following jj v0.41, the pattern is treated
|
|
5061
|
+
* as a regular expression by default; pass `{ kind: 'substring' }` for a
|
|
5062
|
+
* literal search.
|
|
5063
|
+
*
|
|
5064
|
+
* @param {Record<string, any>} args - Arguments
|
|
5065
|
+
* @returns {Promise<Array<{path: string, lineNumber: number, line: string}>>}
|
|
5066
|
+
*/
|
|
5067
|
+
async search(args) {
|
|
5068
|
+
if (!args || !args.pattern) {
|
|
5069
|
+
throw new JJError('INVALID_ARGUMENT', 'Missing pattern argument', {
|
|
5070
|
+
suggestion: 'Provide a search pattern: { pattern: "TODO" }',
|
|
5071
|
+
});
|
|
5072
|
+
}
|
|
5073
|
+
|
|
5074
|
+
// v1.6: when searching the working copy, snapshot the disk first so
|
|
5075
|
+
// out-of-band files are searchable.
|
|
5076
|
+
if (!args.changeId && !args.change) {
|
|
5077
|
+
await autoSnapshotWorkingCopy();
|
|
5078
|
+
}
|
|
5079
|
+
|
|
5080
|
+
await graph.load();
|
|
5081
|
+
const changeId = args.changeId || args.change || workingCopy.getCurrentChangeId();
|
|
5082
|
+
const change = await graph.getChange(changeId);
|
|
5083
|
+
if (!change) {
|
|
5084
|
+
throw new JJError('CHANGE_NOT_FOUND', `Change ${changeId} not found`);
|
|
5085
|
+
}
|
|
5086
|
+
|
|
5087
|
+
const kind = args.kind || 'regex';
|
|
5088
|
+
let matcher;
|
|
5089
|
+
if (kind === 'substring') {
|
|
5090
|
+
matcher = (/** @type {any} */ line) => line.includes(args.pattern);
|
|
5091
|
+
} else {
|
|
5092
|
+
let regex;
|
|
5093
|
+
try {
|
|
5094
|
+
regex = new RegExp(args.pattern);
|
|
5095
|
+
} catch (error) {
|
|
5096
|
+
throw new JJError('INVALID_PATTERN', `Invalid regex pattern: ${error.message}`, {
|
|
5097
|
+
pattern: args.pattern,
|
|
5098
|
+
});
|
|
5099
|
+
}
|
|
5100
|
+
matcher = (/** @type {any} */ line) => regex.test(line);
|
|
5101
|
+
}
|
|
5102
|
+
|
|
5103
|
+
const snapshot = change.fileSnapshot || {};
|
|
5104
|
+
/** @type {any[]} */
|
|
5105
|
+
const results = [];
|
|
5106
|
+
for (const [filePath, content] of Object.entries(snapshot)) {
|
|
5107
|
+
if (args.path && filePath !== args.path) continue;
|
|
5108
|
+
if (typeof content !== 'string') continue; // skip binary
|
|
5109
|
+
const lines = content.split('\n');
|
|
5110
|
+
lines.forEach((line, index) => {
|
|
5111
|
+
if (matcher(line)) {
|
|
5112
|
+
results.push({ path: filePath, lineNumber: index + 1, line });
|
|
5113
|
+
}
|
|
5114
|
+
});
|
|
5115
|
+
}
|
|
5116
|
+
return results;
|
|
5117
|
+
},
|
|
5118
|
+
|
|
4767
5119
|
/**
|
|
4768
5120
|
* Write file content (organized file operation)
|
|
4769
5121
|
*
|
|
4770
|
-
* @param {
|
|
4771
|
-
* @param {string} args.path - File path to write
|
|
4772
|
-
* @param {string|Uint8Array} args.data - File content
|
|
5122
|
+
* @param {Record<string, any>} args - Arguments
|
|
4773
5123
|
* @returns {Promise<Object>} Write result with file info
|
|
4774
5124
|
*/
|
|
4775
5125
|
async write(args) {
|
|
@@ -4779,9 +5129,7 @@ export async function createJJ(options) {
|
|
|
4779
5129
|
/**
|
|
4780
5130
|
* Move/rename file (organized file operation)
|
|
4781
5131
|
*
|
|
4782
|
-
* @param {
|
|
4783
|
-
* @param {string} args.from - Source path
|
|
4784
|
-
* @param {string} args.to - Destination path
|
|
5132
|
+
* @param {Record<string, any>} args - Arguments
|
|
4785
5133
|
* @returns {Promise<Object>} Move result
|
|
4786
5134
|
*/
|
|
4787
5135
|
async move(args) {
|
|
@@ -4791,8 +5139,7 @@ export async function createJJ(options) {
|
|
|
4791
5139
|
/**
|
|
4792
5140
|
* Remove file (organized file operation)
|
|
4793
5141
|
*
|
|
4794
|
-
* @param {
|
|
4795
|
-
* @param {string} args.path - File path to remove
|
|
5142
|
+
* @param {Record<string, any>} args - Arguments
|
|
4796
5143
|
* @returns {Promise<Object>} Remove result with file info
|
|
4797
5144
|
*/
|
|
4798
5145
|
async remove(args) {
|
|
@@ -4802,10 +5149,8 @@ export async function createJJ(options) {
|
|
|
4802
5149
|
/**
|
|
4803
5150
|
* Show which revision modified each line (matches `jj file annotate` / git blame)
|
|
4804
5151
|
*
|
|
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
|
|
5152
|
+
* @param {Record<string, any>} args - Arguments
|
|
5153
|
+
* @returns {Promise<Array<any>>} Array of line annotations with changeId, author, timestamp, and content
|
|
4809
5154
|
*/
|
|
4810
5155
|
async annotate(args) {
|
|
4811
5156
|
if (!args || !args.path) {
|
|
@@ -4847,7 +5192,8 @@ export async function createJJ(options) {
|
|
|
4847
5192
|
}
|
|
4848
5193
|
|
|
4849
5194
|
// Move to parent (simplified - takes first parent)
|
|
4850
|
-
currentChangeId =
|
|
5195
|
+
currentChangeId =
|
|
5196
|
+
change.parents && change.parents.length > 0 ? change.parents[0] : null;
|
|
4851
5197
|
}
|
|
4852
5198
|
|
|
4853
5199
|
annotations.push({
|
|
@@ -4866,9 +5212,7 @@ export async function createJJ(options) {
|
|
|
4866
5212
|
* Change file permissions (matches `jj file chmod`)
|
|
4867
5213
|
* Note: Only works in Node.js, not in browsers
|
|
4868
5214
|
*
|
|
4869
|
-
* @param {
|
|
4870
|
-
* @param {string} args.path - File path
|
|
4871
|
-
* @param {number|string} args.mode - File mode (e.g., 0o755 or '755')
|
|
5215
|
+
* @param {Record<string, any>} args - Arguments
|
|
4872
5216
|
* @returns {Promise<Object>} chmod result
|
|
4873
5217
|
*/
|
|
4874
5218
|
async chmod(args) {
|
|
@@ -4886,9 +5230,13 @@ export async function createJJ(options) {
|
|
|
4886
5230
|
|
|
4887
5231
|
// Check if we're in a browser environment
|
|
4888
5232
|
if (typeof process === 'undefined' || !fs.promises.chmod) {
|
|
4889
|
-
throw new JJError(
|
|
4890
|
-
|
|
4891
|
-
|
|
5233
|
+
throw new JJError(
|
|
5234
|
+
'UNSUPPORTED_OPERATION',
|
|
5235
|
+
'chmod is not supported in browser environments',
|
|
5236
|
+
{
|
|
5237
|
+
suggestion: 'chmod only works in Node.js',
|
|
5238
|
+
}
|
|
5239
|
+
);
|
|
4892
5240
|
}
|
|
4893
5241
|
|
|
4894
5242
|
const fullPath = path.join(dir, args.path);
|
|
@@ -4914,11 +5262,10 @@ export async function createJJ(options) {
|
|
|
4914
5262
|
* This is a stub method that throws an error explaining why explicit
|
|
4915
5263
|
* file tracking is not needed in JavaScript environments.
|
|
4916
5264
|
*
|
|
4917
|
-
* @param {
|
|
4918
|
-
* @param {string} args.path - File path to track
|
|
5265
|
+
* @param {Record<string, any>} _args - Arguments
|
|
4919
5266
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
4920
5267
|
*/
|
|
4921
|
-
async track(
|
|
5268
|
+
async track(_args) {
|
|
4922
5269
|
throw new JJError(
|
|
4923
5270
|
'UNSUPPORTED_OPERATION',
|
|
4924
5271
|
'Explicit file tracking (jj file track) is not needed in JavaScript environments',
|
|
@@ -4945,11 +5292,10 @@ export async function createJJ(options) {
|
|
|
4945
5292
|
* This is a stub method that throws an error explaining why explicit
|
|
4946
5293
|
* file untracking is not needed in JavaScript environments.
|
|
4947
5294
|
*
|
|
4948
|
-
* @param {
|
|
4949
|
-
* @param {string} args.path - File path to untrack
|
|
5295
|
+
* @param {Record<string, any>} _args - Arguments
|
|
4950
5296
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
4951
5297
|
*/
|
|
4952
|
-
async untrack(
|
|
5298
|
+
async untrack(_args) {
|
|
4953
5299
|
throw new JJError(
|
|
4954
5300
|
'UNSUPPORTED_OPERATION',
|
|
4955
5301
|
'Explicit file untracking (jj file untrack) is not needed in JavaScript environments',
|
|
@@ -4979,10 +5325,7 @@ export async function createJJ(options) {
|
|
|
4979
5325
|
/**
|
|
4980
5326
|
* Add a new workspace
|
|
4981
5327
|
*
|
|
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
|
|
5328
|
+
* @param {Record<string, any>} args - Workspace arguments
|
|
4986
5329
|
*/
|
|
4987
5330
|
async add(args) {
|
|
4988
5331
|
await workspaces.load();
|
|
@@ -4993,7 +5336,7 @@ export async function createJJ(options) {
|
|
|
4993
5336
|
args = { ...args, changeId: currentChangeId };
|
|
4994
5337
|
}
|
|
4995
5338
|
|
|
4996
|
-
const workspace = await workspaces.add(args);
|
|
5339
|
+
const workspace = /** @type {any} */ (await workspaces.add(/** @type {any} */ (args)));
|
|
4997
5340
|
|
|
4998
5341
|
// If a change was specified, check it out in the new workspace
|
|
4999
5342
|
if (args.changeId) {
|
|
@@ -5050,9 +5393,7 @@ export async function createJJ(options) {
|
|
|
5050
5393
|
/**
|
|
5051
5394
|
* Remove a workspace
|
|
5052
5395
|
*
|
|
5053
|
-
* @param {
|
|
5054
|
-
* @param {string} args.id - Workspace ID
|
|
5055
|
-
* @param {boolean} [args.force=false] - Force removal
|
|
5396
|
+
* @param {Record<string, any>} args - Remove arguments
|
|
5056
5397
|
*/
|
|
5057
5398
|
async remove(args) {
|
|
5058
5399
|
if (!args || !args.id) {
|
|
@@ -5082,8 +5423,7 @@ export async function createJJ(options) {
|
|
|
5082
5423
|
/**
|
|
5083
5424
|
* Forget a workspace without removing files (matches `jj workspace forget`)
|
|
5084
5425
|
*
|
|
5085
|
-
* @param {
|
|
5086
|
-
* @param {string} args.id - Workspace ID
|
|
5426
|
+
* @param {Record<string, any>} args - Forget arguments
|
|
5087
5427
|
*/
|
|
5088
5428
|
async forget(args) {
|
|
5089
5429
|
if (!args || !args.id) {
|
|
@@ -5115,7 +5455,7 @@ export async function createJJ(options) {
|
|
|
5115
5455
|
*/
|
|
5116
5456
|
async list() {
|
|
5117
5457
|
await workspaces.load();
|
|
5118
|
-
return workspaces.list();
|
|
5458
|
+
return /** @type {any[]} */ (workspaces.list());
|
|
5119
5459
|
},
|
|
5120
5460
|
|
|
5121
5461
|
/**
|
|
@@ -5125,7 +5465,7 @@ export async function createJJ(options) {
|
|
|
5125
5465
|
*/
|
|
5126
5466
|
async get(id) {
|
|
5127
5467
|
await workspaces.load();
|
|
5128
|
-
const workspace = workspaces.get(id);
|
|
5468
|
+
const workspace = /** @type {any} */ (workspaces.get(id));
|
|
5129
5469
|
if (!workspace) {
|
|
5130
5470
|
throw new JJError('WORKSPACE_NOT_FOUND', `Workspace ${id} not found`);
|
|
5131
5471
|
}
|
|
@@ -5135,9 +5475,7 @@ export async function createJJ(options) {
|
|
|
5135
5475
|
/**
|
|
5136
5476
|
* Rename a workspace (matches `jj workspace rename`)
|
|
5137
5477
|
*
|
|
5138
|
-
* @param {
|
|
5139
|
-
* @param {string} args.workspace - Current workspace name/id
|
|
5140
|
-
* @param {string} args.newName - New name for the workspace
|
|
5478
|
+
* @param {Record<string, any>} args - Rename arguments
|
|
5141
5479
|
* @returns {Promise<Object>} Updated workspace
|
|
5142
5480
|
*/
|
|
5143
5481
|
async rename(args) {
|
|
@@ -5146,7 +5484,7 @@ export async function createJJ(options) {
|
|
|
5146
5484
|
}
|
|
5147
5485
|
|
|
5148
5486
|
await workspaces.load();
|
|
5149
|
-
const workspace = workspaces.get(args.workspace);
|
|
5487
|
+
const workspace = /** @type {any} */ (workspaces.get(args.workspace));
|
|
5150
5488
|
if (!workspace) {
|
|
5151
5489
|
throw new JJError('WORKSPACE_NOT_FOUND', `Workspace ${args.workspace} not found`);
|
|
5152
5490
|
}
|
|
@@ -5174,8 +5512,7 @@ export async function createJJ(options) {
|
|
|
5174
5512
|
/**
|
|
5175
5513
|
* Get workspace root directory (matches `jj workspace root`)
|
|
5176
5514
|
*
|
|
5177
|
-
* @param {
|
|
5178
|
-
* @param {string} [args.workspace] - Workspace name/id (defaults to current)
|
|
5515
|
+
* @param {Record<string, any>} [args={}] - Arguments
|
|
5179
5516
|
* @returns {Promise<string>} Root directory path
|
|
5180
5517
|
*/
|
|
5181
5518
|
async root(args = {}) {
|
|
@@ -5187,10 +5524,10 @@ export async function createJJ(options) {
|
|
|
5187
5524
|
await workspaces.load();
|
|
5188
5525
|
|
|
5189
5526
|
// Try to get by ID first, then by name
|
|
5190
|
-
let workspace = workspaces.get(args.workspace);
|
|
5527
|
+
let workspace = /** @type {any} */ (workspaces.get(args.workspace));
|
|
5191
5528
|
if (!workspace) {
|
|
5192
5529
|
// Search by name
|
|
5193
|
-
const allWorkspaces = workspaces.list();
|
|
5530
|
+
const allWorkspaces = /** @type {any[]} */ (workspaces.list());
|
|
5194
5531
|
workspace = allWorkspaces.find((ws) => ws.name === args.workspace);
|
|
5195
5532
|
}
|
|
5196
5533
|
|
|
@@ -5204,15 +5541,14 @@ export async function createJJ(options) {
|
|
|
5204
5541
|
/**
|
|
5205
5542
|
* Update stale workspaces (matches `jj workspace update-stale`)
|
|
5206
5543
|
*
|
|
5207
|
-
* @param {
|
|
5208
|
-
* @param {string} [args.workspace] - Specific workspace to update (defaults to all stale)
|
|
5544
|
+
* @param {Record<string, any>} [args={}] - Arguments
|
|
5209
5545
|
* @returns {Promise<Object>} Update result
|
|
5210
5546
|
*/
|
|
5211
5547
|
async updateStale(args = {}) {
|
|
5212
5548
|
await workspaces.load();
|
|
5213
5549
|
await graph.load();
|
|
5214
5550
|
|
|
5215
|
-
const allWorkspaces = workspaces.list();
|
|
5551
|
+
const allWorkspaces = /** @type {any[]} */ (workspaces.list());
|
|
5216
5552
|
const staleWorkspaces = [];
|
|
5217
5553
|
|
|
5218
5554
|
for (const ws of allWorkspaces) {
|
|
@@ -5235,9 +5571,9 @@ export async function createJJ(options) {
|
|
|
5235
5571
|
|
|
5236
5572
|
if (args.workspace && toUpdate.length === 0) {
|
|
5237
5573
|
// Look up the workspace to get its name for a better error message
|
|
5238
|
-
let workspace = workspaces.get(args.workspace);
|
|
5574
|
+
let workspace = /** @type {any} */ (workspaces.get(args.workspace));
|
|
5239
5575
|
if (!workspace) {
|
|
5240
|
-
const allWorkspaces = workspaces.list();
|
|
5576
|
+
const allWorkspaces = /** @type {any[]} */ (workspaces.list());
|
|
5241
5577
|
workspace = allWorkspaces.find((ws) => ws.name === args.workspace);
|
|
5242
5578
|
}
|
|
5243
5579
|
const workspaceName = workspace ? workspace.name : args.workspace;
|
|
@@ -5305,7 +5641,7 @@ export async function createJJ(options) {
|
|
|
5305
5641
|
* Queue a background operation
|
|
5306
5642
|
*
|
|
5307
5643
|
* @param {Function} operation - Async operation
|
|
5308
|
-
* @param {
|
|
5644
|
+
* @param {Record<string, any>} [opts] - Options
|
|
5309
5645
|
*/
|
|
5310
5646
|
async queue(operation, opts) {
|
|
5311
5647
|
if (!jj.backgroundOps) {
|
|
@@ -5319,7 +5655,7 @@ export async function createJJ(options) {
|
|
|
5319
5655
|
/**
|
|
5320
5656
|
* List background operations
|
|
5321
5657
|
*
|
|
5322
|
-
* @param {
|
|
5658
|
+
* @param {Record<string, any>} [opts] - Filter options
|
|
5323
5659
|
*/
|
|
5324
5660
|
listOperations(opts) {
|
|
5325
5661
|
if (!jj.backgroundOps) {
|
|
@@ -5331,7 +5667,7 @@ export async function createJJ(options) {
|
|
|
5331
5667
|
/**
|
|
5332
5668
|
* Enable auto-snapshot on file changes
|
|
5333
5669
|
*
|
|
5334
|
-
* @param {
|
|
5670
|
+
* @param {Record<string, any>} [opts] - Options
|
|
5335
5671
|
*/
|
|
5336
5672
|
async enableAutoSnapshot(opts) {
|
|
5337
5673
|
if (!jj.backgroundOps) {
|
|
@@ -5373,7 +5709,7 @@ export async function createJJ(options) {
|
|
|
5373
5709
|
/**
|
|
5374
5710
|
* List all bookmarks
|
|
5375
5711
|
*
|
|
5376
|
-
* @returns {Promise<Array
|
|
5712
|
+
* @returns {Promise<Array<any>>} Array of bookmarks with name and changeId
|
|
5377
5713
|
*/
|
|
5378
5714
|
async list() {
|
|
5379
5715
|
await bookmarks.load();
|
|
@@ -5383,9 +5719,7 @@ export async function createJJ(options) {
|
|
|
5383
5719
|
/**
|
|
5384
5720
|
* Set/create a bookmark (matches `jj bookmark set`)
|
|
5385
5721
|
*
|
|
5386
|
-
* @param {
|
|
5387
|
-
* @param {string} args.name - Bookmark name
|
|
5388
|
-
* @param {string} args.changeId - Change ID to point to
|
|
5722
|
+
* @param {Record<string, any>} args - Bookmark arguments
|
|
5389
5723
|
* @returns {Promise<Object>} Bookmark info
|
|
5390
5724
|
*/
|
|
5391
5725
|
async set(args) {
|
|
@@ -5403,7 +5737,8 @@ export async function createJJ(options) {
|
|
|
5403
5737
|
if (!args || !args.name || !args.changeId) {
|
|
5404
5738
|
throw new JJError('INVALID_ARGUMENT', 'Missing name or changeId', {
|
|
5405
5739
|
args,
|
|
5406
|
-
suggestion:
|
|
5740
|
+
suggestion:
|
|
5741
|
+
'Provide both: { name: "main", changeId: "abc123..." } (or use target/change/revision instead of changeId)',
|
|
5407
5742
|
});
|
|
5408
5743
|
}
|
|
5409
5744
|
|
|
@@ -5433,9 +5768,7 @@ export async function createJJ(options) {
|
|
|
5433
5768
|
*
|
|
5434
5769
|
* Unlike `set()`, this will fail if the bookmark already exists.
|
|
5435
5770
|
*
|
|
5436
|
-
* @param {
|
|
5437
|
-
* @param {string} args.name - Bookmark name
|
|
5438
|
-
* @param {string} [args.changeId] - Change ID to point to (default: working copy)
|
|
5771
|
+
* @param {Record<string, any>} args - Bookmark arguments
|
|
5439
5772
|
* @returns {Promise<Object>} Bookmark info
|
|
5440
5773
|
*/
|
|
5441
5774
|
async create(args) {
|
|
@@ -5452,7 +5785,8 @@ export async function createJJ(options) {
|
|
|
5452
5785
|
|
|
5453
5786
|
if (!args || !args.name) {
|
|
5454
5787
|
throw new JJError('INVALID_ARGUMENT', 'Missing name argument', {
|
|
5455
|
-
suggestion:
|
|
5788
|
+
suggestion:
|
|
5789
|
+
'Provide: { name: "main", changeId: "abc123..." } or { name: "main" } for working copy',
|
|
5456
5790
|
});
|
|
5457
5791
|
}
|
|
5458
5792
|
|
|
@@ -5469,7 +5803,8 @@ export async function createJJ(options) {
|
|
|
5469
5803
|
const existingBookmark = await bookmarks.get(args.name);
|
|
5470
5804
|
if (existingBookmark) {
|
|
5471
5805
|
throw new JJError('BOOKMARK_EXISTS', `Bookmark ${args.name} already exists`, {
|
|
5472
|
-
suggestion:
|
|
5806
|
+
suggestion:
|
|
5807
|
+
'Use bookmark.set() to update an existing bookmark, or bookmark.move() to move it',
|
|
5473
5808
|
});
|
|
5474
5809
|
}
|
|
5475
5810
|
|
|
@@ -5496,9 +5831,7 @@ export async function createJJ(options) {
|
|
|
5496
5831
|
/**
|
|
5497
5832
|
* Move a bookmark to a different change (matches `jj bookmark move`)
|
|
5498
5833
|
*
|
|
5499
|
-
* @param {
|
|
5500
|
-
* @param {string} args.name - Bookmark name
|
|
5501
|
-
* @param {string} args.to - Target change ID
|
|
5834
|
+
* @param {Record<string, any>} args - Bookmark arguments
|
|
5502
5835
|
* @returns {Promise<Object>} Move result
|
|
5503
5836
|
*/
|
|
5504
5837
|
async move(args) {
|
|
@@ -5518,7 +5851,8 @@ export async function createJJ(options) {
|
|
|
5518
5851
|
if (!args || !args.name || !args.to) {
|
|
5519
5852
|
throw new JJError('INVALID_ARGUMENT', 'Missing name or to', {
|
|
5520
5853
|
args,
|
|
5521
|
-
suggestion:
|
|
5854
|
+
suggestion:
|
|
5855
|
+
'Provide both: { name: "main", to: "abc123..." } (or use target/changeId/change/revision instead of to)',
|
|
5522
5856
|
});
|
|
5523
5857
|
}
|
|
5524
5858
|
|
|
@@ -5547,11 +5881,68 @@ export async function createJJ(options) {
|
|
|
5547
5881
|
return { name: args.name, from: bookmarkChangeId, to: args.to };
|
|
5548
5882
|
},
|
|
5549
5883
|
|
|
5884
|
+
/**
|
|
5885
|
+
* Advance a bookmark to a descendant change (matches `jj bookmark advance`,
|
|
5886
|
+
* jj v0.39). Unlike move(), advance() only moves the bookmark *forward*
|
|
5887
|
+
* along the graph — the target must be a descendant of the bookmark's
|
|
5888
|
+
* current position (defaults to the working copy).
|
|
5889
|
+
*
|
|
5890
|
+
* @param {Record<string, any>} args - Arguments
|
|
5891
|
+
* @returns {Promise<Object>} { name, from, to }
|
|
5892
|
+
*/
|
|
5893
|
+
async advance(args) {
|
|
5894
|
+
if (args) {
|
|
5895
|
+
const alias = args.to || args.target || args.changeId || args.change || args.revision;
|
|
5896
|
+
if (alias) args = { ...args, to: alias };
|
|
5897
|
+
}
|
|
5898
|
+
if (!args || !args.name) {
|
|
5899
|
+
throw new JJError('INVALID_ARGUMENT', 'Missing bookmark name', {
|
|
5900
|
+
suggestion: 'Provide the bookmark name: { name: "main", to: "abc123..." }',
|
|
5901
|
+
});
|
|
5902
|
+
}
|
|
5903
|
+
|
|
5904
|
+
await bookmarks.load();
|
|
5905
|
+
await graph.load();
|
|
5906
|
+
|
|
5907
|
+
const from = await bookmarks.get(args.name);
|
|
5908
|
+
if (!from) {
|
|
5909
|
+
throw new JJError('NOT_FOUND', `Bookmark ${args.name} not found`);
|
|
5910
|
+
}
|
|
5911
|
+
|
|
5912
|
+
const to = args.to || workingCopy.getCurrentChangeId();
|
|
5913
|
+
|
|
5914
|
+
// The target must be a descendant of (or equal to) the current position.
|
|
5915
|
+
const descendants = new Set(await revset.getDescendants(from));
|
|
5916
|
+
if (to !== from && !descendants.has(to)) {
|
|
5917
|
+
throw new JJError(
|
|
5918
|
+
'BOOKMARK_NOT_ADVANCEABLE',
|
|
5919
|
+
`Cannot advance bookmark ${args.name}: ${to.slice(0, 8)} is not a descendant of ${from.slice(0, 8)}`,
|
|
5920
|
+
{ suggestion: 'Use bookmark.move() to move a bookmark backward or sideways' }
|
|
5921
|
+
);
|
|
5922
|
+
}
|
|
5923
|
+
|
|
5924
|
+
await bookmarks.move(args.name, to);
|
|
5925
|
+
|
|
5926
|
+
await oplog.recordOperation({
|
|
5927
|
+
timestamp: new Date().toISOString(),
|
|
5928
|
+
user: await getUserOplogInfo(),
|
|
5929
|
+
description: `bookmark advance ${args.name}`,
|
|
5930
|
+
parents: [],
|
|
5931
|
+
view: {
|
|
5932
|
+
bookmarks: { [args.name]: to },
|
|
5933
|
+
remoteBookmarks: {},
|
|
5934
|
+
heads: [],
|
|
5935
|
+
workingCopy: workingCopy.getCurrentChangeId(),
|
|
5936
|
+
},
|
|
5937
|
+
});
|
|
5938
|
+
|
|
5939
|
+
return { name: args.name, from, to };
|
|
5940
|
+
},
|
|
5941
|
+
|
|
5550
5942
|
/**
|
|
5551
5943
|
* Delete a bookmark (matches `jj bookmark delete`)
|
|
5552
5944
|
*
|
|
5553
|
-
* @param {
|
|
5554
|
-
* @param {string} args.name - Bookmark name to delete
|
|
5945
|
+
* @param {Record<string, any>} args - Bookmark arguments
|
|
5555
5946
|
* @returns {Promise<Object>} Deletion result
|
|
5556
5947
|
*/
|
|
5557
5948
|
async delete(args) {
|
|
@@ -5562,7 +5953,7 @@ export async function createJJ(options) {
|
|
|
5562
5953
|
}
|
|
5563
5954
|
|
|
5564
5955
|
await bookmarks.load();
|
|
5565
|
-
const bookmark = bookmarks.get(args.name);
|
|
5956
|
+
const bookmark = await bookmarks.get(args.name);
|
|
5566
5957
|
if (!bookmark) {
|
|
5567
5958
|
throw new JJError('NOT_FOUND', `Bookmark ${args.name} not found`);
|
|
5568
5959
|
}
|
|
@@ -5590,9 +5981,7 @@ export async function createJJ(options) {
|
|
|
5590
5981
|
/**
|
|
5591
5982
|
* Rename a bookmark (matches `jj bookmark rename`)
|
|
5592
5983
|
*
|
|
5593
|
-
* @param {
|
|
5594
|
-
* @param {string} args.oldName - Current bookmark name
|
|
5595
|
-
* @param {string} args.newName - New bookmark name
|
|
5984
|
+
* @param {Record<string, any>} args - Bookmark arguments
|
|
5596
5985
|
* @returns {Promise<Object>} Rename result
|
|
5597
5986
|
*/
|
|
5598
5987
|
async rename(args) {
|
|
@@ -5633,9 +6022,7 @@ export async function createJJ(options) {
|
|
|
5633
6022
|
/**
|
|
5634
6023
|
* Track a remote bookmark (matches `jj bookmark track`)
|
|
5635
6024
|
*
|
|
5636
|
-
* @param {
|
|
5637
|
-
* @param {string} args.name - Bookmark name to track
|
|
5638
|
-
* @param {string} [args.remote='origin'] - Remote name
|
|
6025
|
+
* @param {Record<string, any>} args - Bookmark arguments
|
|
5639
6026
|
* @returns {Promise<Object>} Track result
|
|
5640
6027
|
*/
|
|
5641
6028
|
async track(args) {
|
|
@@ -5650,6 +6037,7 @@ export async function createJJ(options) {
|
|
|
5650
6037
|
|
|
5651
6038
|
// Store tracking info in bookmarks metadata
|
|
5652
6039
|
await bookmarks.load();
|
|
6040
|
+
/** @type {Record<string, any>} */
|
|
5653
6041
|
const tracking = bookmarks.tracking || {};
|
|
5654
6042
|
tracking[args.name] = { remote, remoteName: args.name };
|
|
5655
6043
|
bookmarks.tracking = tracking;
|
|
@@ -5675,8 +6063,7 @@ export async function createJJ(options) {
|
|
|
5675
6063
|
/**
|
|
5676
6064
|
* Untrack a remote bookmark (matches `jj bookmark untrack`)
|
|
5677
6065
|
*
|
|
5678
|
-
* @param {
|
|
5679
|
-
* @param {string} args.name - Bookmark name to untrack
|
|
6066
|
+
* @param {Record<string, any>} args - Bookmark arguments
|
|
5680
6067
|
* @returns {Promise<Object>} Untrack result
|
|
5681
6068
|
*/
|
|
5682
6069
|
async untrack(args) {
|
|
@@ -5688,6 +6075,7 @@ export async function createJJ(options) {
|
|
|
5688
6075
|
|
|
5689
6076
|
// Remove tracking info
|
|
5690
6077
|
await bookmarks.load();
|
|
6078
|
+
/** @type {Record<string, any>} */
|
|
5691
6079
|
const tracking = bookmarks.tracking || {};
|
|
5692
6080
|
const wasTracking = tracking[args.name];
|
|
5693
6081
|
delete tracking[args.name];
|
|
@@ -5714,9 +6102,7 @@ export async function createJJ(options) {
|
|
|
5714
6102
|
/**
|
|
5715
6103
|
* Forget a remote bookmark (matches `jj bookmark forget`)
|
|
5716
6104
|
*
|
|
5717
|
-
* @param {
|
|
5718
|
-
* @param {string} args.name - Bookmark name to forget
|
|
5719
|
-
* @param {string} [args.remote='origin'] - Remote name
|
|
6105
|
+
* @param {Record<string, any>} args - Bookmark arguments
|
|
5720
6106
|
* @returns {Promise<Object>} Forget result
|
|
5721
6107
|
*/
|
|
5722
6108
|
async forget(args) {
|
|
@@ -5730,6 +6116,7 @@ export async function createJJ(options) {
|
|
|
5730
6116
|
|
|
5731
6117
|
// Untrack and remove local reference
|
|
5732
6118
|
await bookmarks.load();
|
|
6119
|
+
/** @type {Record<string, any>} */
|
|
5733
6120
|
const tracking = bookmarks.tracking || {};
|
|
5734
6121
|
delete tracking[args.name];
|
|
5735
6122
|
bookmarks.tracking = tracking;
|
|
@@ -5776,16 +6163,15 @@ export async function createJJ(options) {
|
|
|
5776
6163
|
* Tags are immutable references that cannot be moved once created.
|
|
5777
6164
|
* Unlike bookmarks, tags are permanent markers typically used for releases.
|
|
5778
6165
|
*
|
|
5779
|
-
* @param {
|
|
5780
|
-
* @param {string} args.name - Tag name
|
|
5781
|
-
* @param {string} [args.changeId] - Change ID to tag (default: working copy)
|
|
6166
|
+
* @param {Record<string, any>} args - Tag arguments
|
|
5782
6167
|
* @returns {Promise<{name: string, changeId: string}>} Created tag
|
|
5783
6168
|
* @throws {JJError} If tag already exists or name is invalid
|
|
5784
6169
|
*/
|
|
5785
6170
|
async create(args) {
|
|
5786
6171
|
if (!args || args.name === undefined || args.name === null) {
|
|
5787
6172
|
throw new JJError('INVALID_ARGUMENT', 'Missing name argument', {
|
|
5788
|
-
suggestion:
|
|
6173
|
+
suggestion:
|
|
6174
|
+
'Provide: { name: "v1.0.0", changeId: "abc123..." } or { name: "v1.0.0" } for working copy',
|
|
5789
6175
|
});
|
|
5790
6176
|
}
|
|
5791
6177
|
|
|
@@ -5816,11 +6202,55 @@ export async function createJJ(options) {
|
|
|
5816
6202
|
return result;
|
|
5817
6203
|
},
|
|
5818
6204
|
|
|
6205
|
+
/**
|
|
6206
|
+
* Create or update (move) a tag (matches `jj tag set`, jj v0.35).
|
|
6207
|
+
*
|
|
6208
|
+
* Unlike create(), set() is an upsert: it will point an existing tag at a
|
|
6209
|
+
* new change instead of erroring.
|
|
6210
|
+
*
|
|
6211
|
+
* @param {Record<string, any>} args - Tag arguments
|
|
6212
|
+
* @returns {Promise<{name: string, changeId: string, updated: boolean}>}
|
|
6213
|
+
*/
|
|
6214
|
+
async set(args) {
|
|
6215
|
+
if (!args || args.name === undefined || args.name === null) {
|
|
6216
|
+
throw new JJError('INVALID_ARGUMENT', 'Missing name argument', {
|
|
6217
|
+
suggestion: 'Provide: { name: "v1.0.0", changeId: "abc123..." }',
|
|
6218
|
+
});
|
|
6219
|
+
}
|
|
6220
|
+
|
|
6221
|
+
let changeId = args.changeId || args.change || args.revision;
|
|
6222
|
+
if (!changeId) {
|
|
6223
|
+
await workingCopy.load();
|
|
6224
|
+
changeId = workingCopy.getCurrentChangeId();
|
|
6225
|
+
}
|
|
6226
|
+
|
|
6227
|
+
await tags.load();
|
|
6228
|
+
const existed = await tags.exists(args.name);
|
|
6229
|
+
if (existed) {
|
|
6230
|
+
await tags.delete(args.name);
|
|
6231
|
+
}
|
|
6232
|
+
const result = await tags.create(args.name, changeId);
|
|
6233
|
+
|
|
6234
|
+
await oplog.recordOperation({
|
|
6235
|
+
timestamp: new Date().toISOString(),
|
|
6236
|
+
user: await getUserOplogInfo(),
|
|
6237
|
+
description: `tag set ${args.name}`,
|
|
6238
|
+
parents: [],
|
|
6239
|
+
view: {
|
|
6240
|
+
bookmarks: {},
|
|
6241
|
+
remoteBookmarks: {},
|
|
6242
|
+
heads: [],
|
|
6243
|
+
workingCopy: workingCopy.getCurrentChangeId(),
|
|
6244
|
+
},
|
|
6245
|
+
});
|
|
6246
|
+
|
|
6247
|
+
return { ...result, updated: existed };
|
|
6248
|
+
},
|
|
6249
|
+
|
|
5819
6250
|
/**
|
|
5820
6251
|
* List all tags
|
|
5821
6252
|
*
|
|
5822
|
-
* @param {
|
|
5823
|
-
* @param {string} [args.pattern] - Optional glob pattern (e.g., "v1*")
|
|
6253
|
+
* @param {Record<string, any>} [args] - Optional filter arguments
|
|
5824
6254
|
* @returns {Promise<Array<{name: string, changeId: string}>>} List of tags
|
|
5825
6255
|
*/
|
|
5826
6256
|
async list(args) {
|
|
@@ -5831,8 +6261,7 @@ export async function createJJ(options) {
|
|
|
5831
6261
|
/**
|
|
5832
6262
|
* Delete a tag
|
|
5833
6263
|
*
|
|
5834
|
-
* @param {
|
|
5835
|
-
* @param {string} args.name - Tag name to delete
|
|
6264
|
+
* @param {Record<string, any>} args - Tag arguments
|
|
5836
6265
|
* @returns {Promise<{deleted: string}>} Deletion result
|
|
5837
6266
|
* @throws {JJError} If tag doesn't exist
|
|
5838
6267
|
*/
|
|
@@ -5870,7 +6299,7 @@ export async function createJJ(options) {
|
|
|
5870
6299
|
/**
|
|
5871
6300
|
* Fetch from remote (alias to jj.git.fetch for convenience)
|
|
5872
6301
|
*
|
|
5873
|
-
* @param {
|
|
6302
|
+
* @param {Record<string, any>} args - Fetch arguments
|
|
5874
6303
|
* @returns {Promise<Object>} Fetch result
|
|
5875
6304
|
*/
|
|
5876
6305
|
async fetch(args) {
|
|
@@ -5880,7 +6309,7 @@ export async function createJJ(options) {
|
|
|
5880
6309
|
/**
|
|
5881
6310
|
* Push to remote (alias to jj.git.push for convenience)
|
|
5882
6311
|
*
|
|
5883
|
-
* @param {
|
|
6312
|
+
* @param {Record<string, any>} args - Push arguments
|
|
5884
6313
|
* @returns {Promise<Object>} Push result
|
|
5885
6314
|
*/
|
|
5886
6315
|
async push(args) {
|
|
@@ -5890,7 +6319,7 @@ export async function createJJ(options) {
|
|
|
5890
6319
|
/**
|
|
5891
6320
|
* Add a remote (alias to jj.git.remote.add for convenience)
|
|
5892
6321
|
*
|
|
5893
|
-
* @param {
|
|
6322
|
+
* @param {Record<string, any>} args - Remote arguments
|
|
5894
6323
|
* @returns {Promise<Object>} Added remote info
|
|
5895
6324
|
*/
|
|
5896
6325
|
async add(args) {
|
|
@@ -5905,8 +6334,7 @@ export async function createJJ(options) {
|
|
|
5905
6334
|
/**
|
|
5906
6335
|
* Get a configuration value (matches `jj config get`)
|
|
5907
6336
|
*
|
|
5908
|
-
* @param {
|
|
5909
|
-
* @param {string} args.name - Config key (e.g., 'user.name')
|
|
6337
|
+
* @param {Record<string, any>} args - Config arguments
|
|
5910
6338
|
* @returns {Promise<any>} Config value
|
|
5911
6339
|
*/
|
|
5912
6340
|
async get(args) {
|
|
@@ -5931,9 +6359,7 @@ export async function createJJ(options) {
|
|
|
5931
6359
|
/**
|
|
5932
6360
|
* Set a configuration value (matches `jj config set`)
|
|
5933
6361
|
*
|
|
5934
|
-
* @param {
|
|
5935
|
-
* @param {string} args.name - Config key (e.g., 'user.name')
|
|
5936
|
-
* @param {any} args.value - Config value
|
|
6362
|
+
* @param {Record<string, any>} args - Config arguments
|
|
5937
6363
|
* @returns {Promise<Object>} Set result
|
|
5938
6364
|
*/
|
|
5939
6365
|
async set(args) {
|
|
@@ -5980,9 +6406,7 @@ export async function createJJ(options) {
|
|
|
5980
6406
|
* v0.35.0: Loads workspace-config.json if present
|
|
5981
6407
|
* v0.36.0: Accepts optional config object for programmatic configuration
|
|
5982
6408
|
*
|
|
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
|
|
6409
|
+
* @param {Record<string, any>} [opts] - Optional config options
|
|
5986
6410
|
* @returns {Promise<void>}
|
|
5987
6411
|
*
|
|
5988
6412
|
* @example
|
|
@@ -6077,8 +6501,7 @@ export async function createJJ(options) {
|
|
|
6077
6501
|
/**
|
|
6078
6502
|
* Set sparse patterns (replaces all existing patterns)
|
|
6079
6503
|
*
|
|
6080
|
-
* @param {
|
|
6081
|
-
* @param {string[]} args.patterns - Array of glob patterns
|
|
6504
|
+
* @param {Record<string, any>} args - Arguments
|
|
6082
6505
|
* @returns {Promise<{patterns: string[]}>} Result with current patterns
|
|
6083
6506
|
*/
|
|
6084
6507
|
async set(args) {
|
|
@@ -6112,8 +6535,7 @@ export async function createJJ(options) {
|
|
|
6112
6535
|
/**
|
|
6113
6536
|
* Add patterns to sparse checkout
|
|
6114
6537
|
*
|
|
6115
|
-
* @param {
|
|
6116
|
-
* @param {string[]} args.patterns - Array of glob patterns to add
|
|
6538
|
+
* @param {Record<string, any>} args - Arguments
|
|
6117
6539
|
* @returns {Promise<{patterns: string[]}>} Result with current patterns
|
|
6118
6540
|
*/
|
|
6119
6541
|
async add(args) {
|
|
@@ -6157,8 +6579,7 @@ export async function createJJ(options) {
|
|
|
6157
6579
|
/**
|
|
6158
6580
|
* Remove patterns from sparse checkout
|
|
6159
6581
|
*
|
|
6160
|
-
* @param {
|
|
6161
|
-
* @param {string[]} args.patterns - Array of glob patterns to remove
|
|
6582
|
+
* @param {Record<string, any>} args - Arguments
|
|
6162
6583
|
* @returns {Promise<{patterns: string[]}>} Result with current patterns
|
|
6163
6584
|
*/
|
|
6164
6585
|
async remove(args) {
|
|
@@ -6172,7 +6593,7 @@ export async function createJJ(options) {
|
|
|
6172
6593
|
const currentPatterns = workingCopy.getSparsePatterns();
|
|
6173
6594
|
|
|
6174
6595
|
// Remove specified patterns
|
|
6175
|
-
const updatedPatterns = currentPatterns.filter(p => !args.patterns.includes(p));
|
|
6596
|
+
const updatedPatterns = currentPatterns.filter((p) => !args.patterns.includes(p));
|
|
6176
6597
|
|
|
6177
6598
|
await workingCopy.setSparsePatterns(updatedPatterns);
|
|
6178
6599
|
await workingCopy.save();
|
|
@@ -6241,9 +6662,7 @@ export async function createJJ(options) {
|
|
|
6241
6662
|
/**
|
|
6242
6663
|
* Start bisect session
|
|
6243
6664
|
*
|
|
6244
|
-
* @param {
|
|
6245
|
-
* @param {string} args.good - Known good change ID
|
|
6246
|
-
* @param {string} args.bad - Known bad change ID
|
|
6665
|
+
* @param {Record<string, any>} args - Bisect arguments
|
|
6247
6666
|
* @returns {Promise<Object>} Bisect state
|
|
6248
6667
|
*/
|
|
6249
6668
|
async start(args) {
|
|
@@ -6256,7 +6675,7 @@ export async function createJJ(options) {
|
|
|
6256
6675
|
await graph.load();
|
|
6257
6676
|
|
|
6258
6677
|
// Check if bisect is already active
|
|
6259
|
-
const existingState = await storage.read('bisect/state.json');
|
|
6678
|
+
const existingState = /** @type {any} */ (await storage.read('bisect/state.json'));
|
|
6260
6679
|
if (existingState && existingState.active) {
|
|
6261
6680
|
throw new JJError('BISECT_ALREADY_ACTIVE', 'Bisect session already in progress', {
|
|
6262
6681
|
suggestion: 'Use bisect.reset() to end current session first',
|
|
@@ -6279,9 +6698,8 @@ export async function createJJ(options) {
|
|
|
6279
6698
|
|
|
6280
6699
|
// Pick the middle change
|
|
6281
6700
|
// If no candidates between good and bad, test bad itself
|
|
6282
|
-
const current =
|
|
6283
|
-
? candidates[Math.floor(candidates.length / 2)]
|
|
6284
|
-
: args.bad;
|
|
6701
|
+
const current =
|
|
6702
|
+
candidates.length > 0 ? candidates[Math.floor(candidates.length / 2)] : args.bad;
|
|
6285
6703
|
|
|
6286
6704
|
const bisectState = {
|
|
6287
6705
|
active: true,
|
|
@@ -6304,7 +6722,7 @@ export async function createJJ(options) {
|
|
|
6304
6722
|
* @returns {Promise<Object>} Updated bisect state
|
|
6305
6723
|
*/
|
|
6306
6724
|
async good() {
|
|
6307
|
-
const bisectState = await storage.read('bisect/state.json');
|
|
6725
|
+
const bisectState = /** @type {any} */ (await storage.read('bisect/state.json'));
|
|
6308
6726
|
|
|
6309
6727
|
if (!bisectState || !bisectState.active) {
|
|
6310
6728
|
throw new JJError('BISECT_NOT_ACTIVE', 'No active bisect session');
|
|
@@ -6329,7 +6747,7 @@ export async function createJJ(options) {
|
|
|
6329
6747
|
* @returns {Promise<Object>} Updated bisect state
|
|
6330
6748
|
*/
|
|
6331
6749
|
async bad() {
|
|
6332
|
-
const bisectState = await storage.read('bisect/state.json');
|
|
6750
|
+
const bisectState = /** @type {any} */ (await storage.read('bisect/state.json'));
|
|
6333
6751
|
|
|
6334
6752
|
if (!bisectState || !bisectState.active) {
|
|
6335
6753
|
throw new JJError('BISECT_NOT_ACTIVE', 'No active bisect session');
|
|
@@ -6354,7 +6772,7 @@ export async function createJJ(options) {
|
|
|
6354
6772
|
* @returns {Promise<Object>} Updated bisect state
|
|
6355
6773
|
*/
|
|
6356
6774
|
async skip() {
|
|
6357
|
-
const bisectState = await storage.read('bisect/state.json');
|
|
6775
|
+
const bisectState = /** @type {any} */ (await storage.read('bisect/state.json'));
|
|
6358
6776
|
|
|
6359
6777
|
if (!bisectState || !bisectState.active) {
|
|
6360
6778
|
throw new JJError('BISECT_NOT_ACTIVE', 'No active bisect session');
|
|
@@ -6362,7 +6780,9 @@ export async function createJJ(options) {
|
|
|
6362
6780
|
|
|
6363
6781
|
// Remove current from candidates
|
|
6364
6782
|
if (bisectState.current) {
|
|
6365
|
-
bisectState.candidates = bisectState.candidates.filter(
|
|
6783
|
+
bisectState.candidates = bisectState.candidates.filter(
|
|
6784
|
+
(/** @type {any} */ c) => c !== bisectState.current
|
|
6785
|
+
);
|
|
6366
6786
|
}
|
|
6367
6787
|
|
|
6368
6788
|
// Pick next candidate
|
|
@@ -6389,7 +6809,7 @@ export async function createJJ(options) {
|
|
|
6389
6809
|
* @returns {Promise<Object>} Bisect state
|
|
6390
6810
|
*/
|
|
6391
6811
|
async status() {
|
|
6392
|
-
const bisectState = await storage.read('bisect/state.json');
|
|
6812
|
+
const bisectState = /** @type {any} */ (await storage.read('bisect/state.json'));
|
|
6393
6813
|
return bisectState || { active: false };
|
|
6394
6814
|
},
|
|
6395
6815
|
|
|
@@ -6406,7 +6826,7 @@ export async function createJJ(options) {
|
|
|
6406
6826
|
|
|
6407
6827
|
// Find all ancestors of bad that are descendants of good
|
|
6408
6828
|
const badAncestors = graph.getAncestors(badId);
|
|
6409
|
-
const
|
|
6829
|
+
const _goodAncestors = graph.getAncestors(goodId);
|
|
6410
6830
|
|
|
6411
6831
|
// Candidates are changes that are:
|
|
6412
6832
|
// - Ancestors of bad (or bad itself)
|
|
@@ -6436,7 +6856,7 @@ export async function createJJ(options) {
|
|
|
6436
6856
|
* Update bisect state after marking good/bad/skip
|
|
6437
6857
|
*
|
|
6438
6858
|
* @private
|
|
6439
|
-
* @param {
|
|
6859
|
+
* @param {any} bisectState - Current bisect state
|
|
6440
6860
|
*/
|
|
6441
6861
|
async _updateBisectState(bisectState) {
|
|
6442
6862
|
await graph.load();
|
|
@@ -6482,7 +6902,8 @@ export async function createJJ(options) {
|
|
|
6482
6902
|
}
|
|
6483
6903
|
|
|
6484
6904
|
bisectState.candidates = newCandidates;
|
|
6485
|
-
bisectState.remaining =
|
|
6905
|
+
bisectState.remaining =
|
|
6906
|
+
newCandidates.length > 0 ? Math.ceil(Math.log2(newCandidates.length + 1)) : 0;
|
|
6486
6907
|
|
|
6487
6908
|
// Pick the middle candidate
|
|
6488
6909
|
if (newCandidates.length > 0) {
|
|
@@ -6495,7 +6916,7 @@ export async function createJJ(options) {
|
|
|
6495
6916
|
// The first bad change is the one in the bad set that has no bad ancestors
|
|
6496
6917
|
for (const badId of bisectState.bad) {
|
|
6497
6918
|
const badAncestors = graph.getAncestors(badId);
|
|
6498
|
-
const hasChildBad = bisectState.bad.some(otherId => {
|
|
6919
|
+
const hasChildBad = bisectState.bad.some((/** @type {any} */ otherId) => {
|
|
6499
6920
|
if (otherId === badId) return false;
|
|
6500
6921
|
return badAncestors.includes(otherId);
|
|
6501
6922
|
});
|
|
@@ -6512,13 +6933,16 @@ export async function createJJ(options) {
|
|
|
6512
6933
|
/**
|
|
6513
6934
|
* Show file differences between revisions (matches `jj diff`)
|
|
6514
6935
|
*
|
|
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
|
|
6936
|
+
* @param {Record<string, any>} [args={}] - Diff arguments
|
|
6519
6937
|
* @returns {Promise<Object>} Diff result with changed files
|
|
6520
6938
|
*/
|
|
6521
6939
|
async diff(args = {}) {
|
|
6940
|
+
// v1.6: snapshot the working copy first so diffs against @ reflect
|
|
6941
|
+
// out-of-band edits (only when diffing the working copy, i.e. no explicit `to`).
|
|
6942
|
+
if (!args.to) {
|
|
6943
|
+
await autoSnapshotWorkingCopy();
|
|
6944
|
+
}
|
|
6945
|
+
|
|
6522
6946
|
await graph.load();
|
|
6523
6947
|
await workingCopy.load();
|
|
6524
6948
|
|
|
@@ -6574,8 +6998,7 @@ export async function createJJ(options) {
|
|
|
6574
6998
|
/**
|
|
6575
6999
|
* Move working copy to next child revision (matches `jj next`)
|
|
6576
7000
|
*
|
|
6577
|
-
* @param {
|
|
6578
|
-
* @param {number} [args.offset=1] - Number of generations to move forward
|
|
7001
|
+
* @param {Record<string, any>} [args={}] - Next arguments
|
|
6579
7002
|
* @returns {Promise<Object>} Updated working copy info
|
|
6580
7003
|
*/
|
|
6581
7004
|
async next(args = {}) {
|
|
@@ -6593,7 +7016,9 @@ export async function createJJ(options) {
|
|
|
6593
7016
|
// Find children
|
|
6594
7017
|
await graph.load();
|
|
6595
7018
|
const allChanges = await graph.getAllChanges();
|
|
6596
|
-
const children = allChanges.filter(
|
|
7019
|
+
const children = allChanges.filter(
|
|
7020
|
+
(/** @type {any} */ c) => c.parents && c.parents.includes(currentChangeId)
|
|
7021
|
+
);
|
|
6597
7022
|
|
|
6598
7023
|
if (children.length === 0) {
|
|
6599
7024
|
throw new JJError('NO_CHILDREN', 'No child revisions found');
|
|
@@ -6604,7 +7029,9 @@ export async function createJJ(options) {
|
|
|
6604
7029
|
|
|
6605
7030
|
// Move forward 'offset' times
|
|
6606
7031
|
for (let i = 1; i < offset; i++) {
|
|
6607
|
-
const nextChildren = allChanges.filter(
|
|
7032
|
+
const nextChildren = allChanges.filter(
|
|
7033
|
+
(/** @type {any} */ c) => c.parents && c.parents.includes(targetChange.changeId)
|
|
7034
|
+
);
|
|
6608
7035
|
if (nextChildren.length === 0) {
|
|
6609
7036
|
throw new JJError('INSUFFICIENT_CHILDREN', `Cannot move forward ${offset} generations`);
|
|
6610
7037
|
}
|
|
@@ -6625,8 +7052,7 @@ export async function createJJ(options) {
|
|
|
6625
7052
|
/**
|
|
6626
7053
|
* Move working copy to previous parent revision (matches `jj prev`)
|
|
6627
7054
|
*
|
|
6628
|
-
* @param {
|
|
6629
|
-
* @param {number} [args.offset=1] - Number of generations to move backward
|
|
7055
|
+
* @param {Record<string, any>} [args={}] - Prev arguments
|
|
6630
7056
|
* @returns {Promise<Object>} Updated working copy info
|
|
6631
7057
|
*/
|
|
6632
7058
|
async prev(args = {}) {
|
|
@@ -6665,9 +7091,7 @@ export async function createJJ(options) {
|
|
|
6665
7091
|
/**
|
|
6666
7092
|
* Create copies of changes (matches `jj duplicate`)
|
|
6667
7093
|
*
|
|
6668
|
-
* @param {
|
|
6669
|
-
* @param {string[]} [args.changes] - Change IDs to duplicate (defaults to working copy)
|
|
6670
|
-
* @param {string} [args.destination] - Where to place duplicates
|
|
7094
|
+
* @param {Record<string, any>} [args={}] - Duplicate arguments
|
|
6671
7095
|
* @returns {Promise<Object>} Duplication result with new change IDs
|
|
6672
7096
|
*/
|
|
6673
7097
|
async duplicate(args = {}) {
|
|
@@ -6676,8 +7100,8 @@ export async function createJJ(options) {
|
|
|
6676
7100
|
await userConfig.load();
|
|
6677
7101
|
|
|
6678
7102
|
// Support both changeId (singular) and changes (plural)
|
|
6679
|
-
const changesToDup =
|
|
6680
|
-
|
|
7103
|
+
const changesToDup =
|
|
7104
|
+
args.changes || (args.changeId ? [args.changeId] : [workingCopy.getCurrentChangeId()]);
|
|
6681
7105
|
const duplicatedChanges = [];
|
|
6682
7106
|
|
|
6683
7107
|
for (const changeId of changesToDup) {
|
|
@@ -6688,7 +7112,7 @@ export async function createJJ(options) {
|
|
|
6688
7112
|
|
|
6689
7113
|
// Create new change as a copy
|
|
6690
7114
|
const newChangeId = generateChangeId();
|
|
6691
|
-
const
|
|
7115
|
+
const _user = /** @type {any} */ (userConfig.getUser());
|
|
6692
7116
|
const newChange = {
|
|
6693
7117
|
...originalChange,
|
|
6694
7118
|
changeId: newChangeId,
|
|
@@ -6713,7 +7137,7 @@ export async function createJJ(options) {
|
|
|
6713
7137
|
view: {
|
|
6714
7138
|
bookmarks: {},
|
|
6715
7139
|
remoteBookmarks: {},
|
|
6716
|
-
heads: duplicatedChanges.map(d => d.duplicate),
|
|
7140
|
+
heads: duplicatedChanges.map((d) => d.duplicate),
|
|
6717
7141
|
workingCopy: workingCopy.getCurrentChangeId(),
|
|
6718
7142
|
},
|
|
6719
7143
|
});
|
|
@@ -6721,17 +7145,14 @@ export async function createJJ(options) {
|
|
|
6721
7145
|
// Return both formats for compatibility
|
|
6722
7146
|
return {
|
|
6723
7147
|
duplicated: duplicatedChanges,
|
|
6724
|
-
changeIds: duplicatedChanges.map(d => d.duplicate)
|
|
7148
|
+
changeIds: duplicatedChanges.map((d) => d.duplicate),
|
|
6725
7149
|
};
|
|
6726
7150
|
},
|
|
6727
7151
|
|
|
6728
7152
|
/**
|
|
6729
7153
|
* Restore paths from another revision (matches `jj restore`)
|
|
6730
7154
|
*
|
|
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)
|
|
7155
|
+
* @param {Record<string, any>} args - Restore arguments
|
|
6735
7156
|
* @returns {Promise<Object>} Restore result
|
|
6736
7157
|
*/
|
|
6737
7158
|
async restore(args = {}) {
|
|
@@ -6793,9 +7214,7 @@ export async function createJJ(options) {
|
|
|
6793
7214
|
* This operation takes multiple changes and gives them the same parent,
|
|
6794
7215
|
* making them siblings in the change graph instead of ancestors/descendants.
|
|
6795
7216
|
*
|
|
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)
|
|
7217
|
+
* @param {Record<string, any>} args - Arguments
|
|
6799
7218
|
* @returns {Promise<Object>} Operation result with parallelized changes
|
|
6800
7219
|
*
|
|
6801
7220
|
* @example
|
|
@@ -6912,7 +7331,7 @@ export async function createJJ(options) {
|
|
|
6912
7331
|
parent: parentId,
|
|
6913
7332
|
};
|
|
6914
7333
|
},
|
|
6915
|
-
};
|
|
7334
|
+
});
|
|
6916
7335
|
|
|
6917
7336
|
// ============================================================================
|
|
6918
7337
|
// UNSUPPORTED FEATURE STUBS
|
|
@@ -6925,17 +7344,14 @@ export async function createJJ(options) {
|
|
|
6925
7344
|
* @status UNSUPPORTED - Requires interactive diff editor
|
|
6926
7345
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
6927
7346
|
*/
|
|
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
|
-
);
|
|
7347
|
+
jj.diffedit = async function (/** @type {any} */ _args) {
|
|
7348
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'diffedit requires interactive diff editor', {
|
|
7349
|
+
feature: 'diffedit',
|
|
7350
|
+
reason: 'Interactive tools not supported in library API',
|
|
7351
|
+
alternative: 'Use jj.diff() to view changes, then jj.edit() to modify the change',
|
|
7352
|
+
suggestion:
|
|
7353
|
+
'For programmatic editing, use jj.write() to update files and jj.describe() to update the description',
|
|
7354
|
+
});
|
|
6939
7355
|
};
|
|
6940
7356
|
|
|
6941
7357
|
/**
|
|
@@ -6944,55 +7360,19 @@ export async function createJJ(options) {
|
|
|
6944
7360
|
* @status UNSUPPORTED - Requires external formatters
|
|
6945
7361
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
6946
7362
|
*/
|
|
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
|
-
);
|
|
7363
|
+
jj.fix = async function (/** @type {any} */ _args) {
|
|
7364
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'Automatic formatting requires external tools', {
|
|
7365
|
+
feature: 'fix',
|
|
7366
|
+
reason: 'External formatters (prettier, eslint, etc.) must be integrated separately',
|
|
7367
|
+
alternative: 'Run formatters separately, then use jj.write() to update files',
|
|
7368
|
+
suggestion: 'Consider using pre-commit hooks to run formatters automatically',
|
|
7369
|
+
});
|
|
6977
7370
|
};
|
|
6978
7371
|
|
|
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
|
-
};
|
|
7372
|
+
// NOTE: jj.sign() / jj.unsign() are implemented as real (metadata-based)
|
|
7373
|
+
// methods in the main API object above (v1.5). The old UNSUPPORTED stubs were
|
|
7374
|
+
// removed — a pure-JS library can't verify keys, but it can record and expose
|
|
7375
|
+
// signature metadata for the signed() revset and Git-layer integration.
|
|
6996
7376
|
|
|
6997
7377
|
/**
|
|
6998
7378
|
* util - CLI utility commands (STUB namespace)
|
|
@@ -7005,15 +7385,11 @@ export async function createJJ(options) {
|
|
|
7005
7385
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
7006
7386
|
*/
|
|
7007
7387
|
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
|
-
);
|
|
7388
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'Shell completion generation is CLI-specific', {
|
|
7389
|
+
feature: 'util.completion',
|
|
7390
|
+
reason: 'Completions are for command-line shells, not library APIs',
|
|
7391
|
+
alternative: 'Not applicable - use JJ CLI for shell completions',
|
|
7392
|
+
});
|
|
7017
7393
|
},
|
|
7018
7394
|
|
|
7019
7395
|
/**
|
|
@@ -7021,31 +7397,23 @@ export async function createJJ(options) {
|
|
|
7021
7397
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
7022
7398
|
*/
|
|
7023
7399
|
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
|
-
);
|
|
7400
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'Garbage collection is a CLI utility', {
|
|
7401
|
+
feature: 'util.gc',
|
|
7402
|
+
reason: 'Storage management handled automatically by isomorphic-jj',
|
|
7403
|
+
alternative: 'Repository cleanup happens automatically',
|
|
7404
|
+
});
|
|
7033
7405
|
},
|
|
7034
7406
|
|
|
7035
7407
|
/**
|
|
7036
7408
|
* exec - Execute command with JJ environment (STUB)
|
|
7037
7409
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
7038
7410
|
*/
|
|
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
|
-
);
|
|
7411
|
+
async exec(/** @type {any} */ _args) {
|
|
7412
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'Command execution is a CLI utility', {
|
|
7413
|
+
feature: 'util.exec',
|
|
7414
|
+
reason: 'Shell command execution not appropriate for library API',
|
|
7415
|
+
alternative: 'Use Node.js child_process module directly if needed',
|
|
7416
|
+
});
|
|
7049
7417
|
},
|
|
7050
7418
|
|
|
7051
7419
|
/**
|
|
@@ -7053,15 +7421,11 @@ export async function createJJ(options) {
|
|
|
7053
7421
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
7054
7422
|
*/
|
|
7055
7423
|
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
|
-
);
|
|
7424
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'Config schema output is a CLI utility', {
|
|
7425
|
+
feature: 'util.configSchema',
|
|
7426
|
+
reason: 'Schema introspection not needed for programmatic usage',
|
|
7427
|
+
alternative: 'See TypeScript types or API documentation for config structure',
|
|
7428
|
+
});
|
|
7065
7429
|
},
|
|
7066
7430
|
};
|
|
7067
7431
|
|
|
@@ -7075,17 +7439,13 @@ export async function createJJ(options) {
|
|
|
7075
7439
|
* upload - Upload changes to Gerrit (STUB)
|
|
7076
7440
|
* @throws {JJError} Always throws UNSUPPORTED_OPERATION
|
|
7077
7441
|
*/
|
|
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
|
-
);
|
|
7442
|
+
async upload(/** @type {any} */ _args) {
|
|
7443
|
+
throw new JJError('UNSUPPORTED_OPERATION', 'Gerrit integration not supported', {
|
|
7444
|
+
feature: 'gerrit.upload',
|
|
7445
|
+
reason: 'Gerrit-specific features not implemented',
|
|
7446
|
+
alternative: 'Use jj.git.push() with appropriate Gerrit remote configuration',
|
|
7447
|
+
suggestion: 'Configure Gerrit remote and use standard Git push with refs/for/ syntax',
|
|
7448
|
+
});
|
|
7089
7449
|
},
|
|
7090
7450
|
};
|
|
7091
7451
|
|