isomorphic-jj 1.1.3 → 1.3.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/README.md +48 -0
- package/dist/index.cjs +327 -156
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +327 -156
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/api/repository.js +246 -150
- package/src/core/bookmark-store.js +16 -4
- package/src/core/revset-engine.js +18 -0
- package/src/core/user-config.js +47 -2
- package/src/types.d.ts +165 -2
package/dist/index.cjs
CHANGED
|
@@ -1312,6 +1312,7 @@ class BookmarkStore {
|
|
|
1312
1312
|
this.storage = storage;
|
|
1313
1313
|
this.local = new Map();
|
|
1314
1314
|
this.remote = new Map(); // remote → (bookmark → changeId)
|
|
1315
|
+
this.tracking = {}; // bookmark → { remote, ref } tracking info (v0.35.0)
|
|
1315
1316
|
}
|
|
1316
1317
|
|
|
1317
1318
|
/**
|
|
@@ -1354,6 +1355,7 @@ class BookmarkStore {
|
|
|
1354
1355
|
|
|
1355
1356
|
this.local.clear();
|
|
1356
1357
|
this.remote.clear();
|
|
1358
|
+
this.tracking = data.tracked || {}; // Load tracking info (v0.35.0)
|
|
1357
1359
|
|
|
1358
1360
|
// Load local bookmarks
|
|
1359
1361
|
for (const [name, target] of Object.entries(data.local)) {
|
|
@@ -1391,7 +1393,7 @@ class BookmarkStore {
|
|
|
1391
1393
|
version: 1,
|
|
1392
1394
|
local,
|
|
1393
1395
|
remote,
|
|
1394
|
-
tracked: {},
|
|
1396
|
+
tracked: this.tracking || {}, // Save tracking info (v0.35.0)
|
|
1395
1397
|
});
|
|
1396
1398
|
}
|
|
1397
1399
|
|
|
@@ -1504,13 +1506,23 @@ class BookmarkStore {
|
|
|
1504
1506
|
async list() {
|
|
1505
1507
|
const result = [];
|
|
1506
1508
|
|
|
1507
|
-
// Add local bookmarks
|
|
1509
|
+
// Add local bookmarks with tracking info (v0.35.0)
|
|
1508
1510
|
for (const [name, target] of this.local.entries()) {
|
|
1509
|
-
|
|
1511
|
+
const bookmark = {
|
|
1510
1512
|
name,
|
|
1511
1513
|
changeId: target,
|
|
1512
1514
|
remote: null,
|
|
1513
|
-
}
|
|
1515
|
+
};
|
|
1516
|
+
|
|
1517
|
+
// Include tracking info if available
|
|
1518
|
+
if (this.tracking && this.tracking[name]) {
|
|
1519
|
+
bookmark.tracking = {
|
|
1520
|
+
remote: this.tracking[name].remote,
|
|
1521
|
+
ref: this.tracking[name].remoteName || name,
|
|
1522
|
+
};
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
result.push(bookmark);
|
|
1514
1526
|
}
|
|
1515
1527
|
|
|
1516
1528
|
// Add remote bookmarks
|
|
@@ -1832,6 +1844,24 @@ class RevsetEngine {
|
|
|
1832
1844
|
}
|
|
1833
1845
|
}
|
|
1834
1846
|
|
|
1847
|
+
// v0.36.0: visible() - non-abandoned changes
|
|
1848
|
+
if (trimmed === 'visible()') {
|
|
1849
|
+
await this.graph.load();
|
|
1850
|
+
const all = this.graph.getAll();
|
|
1851
|
+
return all
|
|
1852
|
+
.filter(c => !c.abandoned)
|
|
1853
|
+
.map(c => c.changeId);
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
// v0.36.0: hidden() - abandoned changes
|
|
1857
|
+
if (trimmed === 'hidden()') {
|
|
1858
|
+
await this.graph.load();
|
|
1859
|
+
const all = this.graph.getAll();
|
|
1860
|
+
return all
|
|
1861
|
+
.filter(c => c.abandoned === true)
|
|
1862
|
+
.map(c => c.changeId);
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1835
1865
|
// ancestors(changeId) - all ancestors including the change itself
|
|
1836
1866
|
const ancestorsMatch = trimmed.match(/^ancestors\(([0-9a-f]{32})\)$/);
|
|
1837
1867
|
if (ancestorsMatch) {
|
|
@@ -4279,9 +4309,15 @@ class UserConfig {
|
|
|
4279
4309
|
}
|
|
4280
4310
|
|
|
4281
4311
|
/**
|
|
4282
|
-
* Load config from storage
|
|
4312
|
+
* Load config from storage and/or programmatic object
|
|
4313
|
+
* v0.35.0: Also loads workspace-specific config from workspace-config.json
|
|
4314
|
+
* v0.36.0: Accepts optional config object for programmatic configuration
|
|
4315
|
+
*
|
|
4316
|
+
* @param {Object} [opts] - Optional config options
|
|
4317
|
+
* @param {Object} [opts.workspace] - Workspace-specific config to merge (highest priority)
|
|
4318
|
+
* @param {Object} [opts.override] - Config object to merge over loaded config
|
|
4283
4319
|
*/
|
|
4284
|
-
async load() {
|
|
4320
|
+
async load(opts = {}) {
|
|
4285
4321
|
try {
|
|
4286
4322
|
const data = await this.storage.read('config.json');
|
|
4287
4323
|
if (data) {
|
|
@@ -4291,6 +4327,45 @@ class UserConfig {
|
|
|
4291
4327
|
// No config file yet, use defaults
|
|
4292
4328
|
await this.init();
|
|
4293
4329
|
}
|
|
4330
|
+
|
|
4331
|
+
// Merge programmatic override config if provided
|
|
4332
|
+
if (opts.override) {
|
|
4333
|
+
this.config = this._deepMerge(this.config, opts.override);
|
|
4334
|
+
}
|
|
4335
|
+
|
|
4336
|
+
// Load workspace-specific config from file (v0.35.0)
|
|
4337
|
+
try {
|
|
4338
|
+
const workspaceData = await this.storage.read('workspace-config.json');
|
|
4339
|
+
if (workspaceData) {
|
|
4340
|
+
// Merge workspace config over global config (workspace takes precedence)
|
|
4341
|
+
this.config = this._deepMerge(this.config, workspaceData);
|
|
4342
|
+
}
|
|
4343
|
+
} catch (error) {
|
|
4344
|
+
// No workspace config, that's fine
|
|
4345
|
+
}
|
|
4346
|
+
|
|
4347
|
+
// Merge programmatic workspace config if provided (highest priority)
|
|
4348
|
+
if (opts.workspace) {
|
|
4349
|
+
this.config = this._deepMerge(this.config, opts.workspace);
|
|
4350
|
+
}
|
|
4351
|
+
}
|
|
4352
|
+
|
|
4353
|
+
/**
|
|
4354
|
+
* Deep merge two config objects (workspace overrides global)
|
|
4355
|
+
* @private
|
|
4356
|
+
*/
|
|
4357
|
+
_deepMerge(target, source) {
|
|
4358
|
+
const result = { ...target };
|
|
4359
|
+
|
|
4360
|
+
for (const key in source) {
|
|
4361
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
4362
|
+
result[key] = this._deepMerge(target[key] || {}, source[key]);
|
|
4363
|
+
} else {
|
|
4364
|
+
result[key] = source[key];
|
|
4365
|
+
}
|
|
4366
|
+
}
|
|
4367
|
+
|
|
4368
|
+
return result;
|
|
4294
4369
|
}
|
|
4295
4370
|
|
|
4296
4371
|
/**
|
|
@@ -6219,69 +6294,116 @@ async function createJJ(options) {
|
|
|
6219
6294
|
* @param {string[]} [args.paths] - Paths to move between changes (for history operations)
|
|
6220
6295
|
* @returns {Promise<Object>} Returns change object for history operations, file info for file operations
|
|
6221
6296
|
*/
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
6297
|
+
/**
|
|
6298
|
+
* Move a file in the working copy
|
|
6299
|
+
*
|
|
6300
|
+
* @param {Object} args - Move file arguments
|
|
6301
|
+
* @param {string} args.from - Source file path
|
|
6302
|
+
* @param {string} args.to - Destination file path
|
|
6303
|
+
* @returns {Promise<Object>} Operation result including from, to paths and file stats
|
|
6304
|
+
*/
|
|
6305
|
+
async moveFile(args) {
|
|
6306
|
+
if (!args.from || typeof args.from !== 'string') {
|
|
6307
|
+
throw new JJError('INVALID_ARGUMENT', 'Missing or invalid from path', {
|
|
6308
|
+
suggestion: 'Provide a valid source file path',
|
|
6309
|
+
provided: { from: args.from, type: typeof args.from },
|
|
6226
6310
|
});
|
|
6227
6311
|
}
|
|
6228
6312
|
|
|
6229
|
-
|
|
6230
|
-
|
|
6313
|
+
if (!args.to || typeof args.to !== 'string') {
|
|
6314
|
+
throw new JJError('INVALID_ARGUMENT', 'Missing or invalid to path', {
|
|
6315
|
+
suggestion: 'Provide a valid destination file path',
|
|
6316
|
+
provided: { to: args.to, type: typeof args.to },
|
|
6317
|
+
});
|
|
6318
|
+
}
|
|
6231
6319
|
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6320
|
+
// Prevent moving to the same location
|
|
6321
|
+
if (args.from === args.to) {
|
|
6322
|
+
throw new JJError('INVALID_OPERATION', 'Source and destination paths are the same', {
|
|
6323
|
+
path: args.from,
|
|
6324
|
+
suggestion: 'Specify a different destination path',
|
|
6325
|
+
});
|
|
6236
6326
|
}
|
|
6237
|
-
},
|
|
6238
6327
|
|
|
6239
|
-
|
|
6240
|
-
|
|
6241
|
-
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
*/
|
|
6246
|
-
_detectHistoryOperation(args) {
|
|
6247
|
-
// Explicit history operation indicators
|
|
6248
|
-
if (args.paths || args.changeId || args.newParent) {
|
|
6249
|
-
return true;
|
|
6328
|
+
// Prevent absolute paths
|
|
6329
|
+
if (args.from.startsWith('/') || args.to.startsWith('/')) {
|
|
6330
|
+
throw new JJError('INVALID_PATH', 'Absolute paths are not allowed', {
|
|
6331
|
+
suggestion: 'Use paths relative to the repository root',
|
|
6332
|
+
provided: { from: args.from, to: args.to },
|
|
6333
|
+
});
|
|
6250
6334
|
}
|
|
6251
6335
|
|
|
6252
|
-
//
|
|
6253
|
-
if (args.from
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6336
|
+
// Prevent parent directory traversal
|
|
6337
|
+
if (args.from.includes('..') || args.to.includes('..')) {
|
|
6338
|
+
throw new JJError('INVALID_PATH', 'Parent directory traversal (..) is not allowed', {
|
|
6339
|
+
suggestion: 'Use paths within the repository',
|
|
6340
|
+
provided: { from: args.from, to: args.to },
|
|
6341
|
+
});
|
|
6342
|
+
}
|
|
6258
6343
|
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
// Users should use explicit { changeId, newParent } for clarity
|
|
6262
|
-
if (fromLooksLikeChangeId && toLooksLikeChangeId) {
|
|
6263
|
-
// This is ambiguous - throw a helpful error
|
|
6264
|
-
throw new JJError('AMBIGUOUS_OPERATION',
|
|
6265
|
-
'Ambiguous move operation: arguments look like change IDs', {
|
|
6266
|
-
suggestion: 'Use { changeId, newParent } for history operations or ensure file paths don\'t match change ID pattern',
|
|
6267
|
-
args,
|
|
6268
|
-
});
|
|
6269
|
-
}
|
|
6344
|
+
const fromPath = `${dir}/${args.from}`;
|
|
6345
|
+
const toPath = `${dir}/${args.to}`;
|
|
6270
6346
|
|
|
6271
|
-
|
|
6347
|
+
// Check if source file exists
|
|
6348
|
+
try {
|
|
6349
|
+
await fs.promises.stat(fromPath);
|
|
6350
|
+
} catch (error) {
|
|
6351
|
+
throw new JJError('FILE_NOT_FOUND', `Source file not found: ${args.from}`, {
|
|
6352
|
+
path: args.from,
|
|
6353
|
+
suggestion: 'Check that the source file exists',
|
|
6354
|
+
});
|
|
6272
6355
|
}
|
|
6273
6356
|
|
|
6274
|
-
|
|
6357
|
+
// Ensure destination directory exists
|
|
6358
|
+
const toDir = toPath.substring(0, toPath.lastIndexOf('/'));
|
|
6359
|
+
try {
|
|
6360
|
+
await fs.promises.mkdir(toDir, { recursive: true });
|
|
6361
|
+
} catch (error) {
|
|
6362
|
+
throw new JJError('FILE_SYSTEM_ERROR', `Failed to create destination directory: ${error.message}`, {
|
|
6363
|
+
path: toDir,
|
|
6364
|
+
error: error.message,
|
|
6365
|
+
});
|
|
6366
|
+
}
|
|
6367
|
+
|
|
6368
|
+
// Move the file
|
|
6369
|
+
try {
|
|
6370
|
+
await fs.promises.rename(fromPath, toPath);
|
|
6371
|
+
} catch (error) {
|
|
6372
|
+
throw new JJError('FILE_SYSTEM_ERROR', `Failed to move file: ${error.message}`, {
|
|
6373
|
+
from: args.from,
|
|
6374
|
+
to: args.to,
|
|
6375
|
+
error: error.message,
|
|
6376
|
+
});
|
|
6377
|
+
}
|
|
6378
|
+
|
|
6379
|
+
// Update working copy tracking
|
|
6380
|
+
await workingCopy.load();
|
|
6381
|
+
await workingCopy.untrackFile(args.from);
|
|
6382
|
+
const stats = await fs.promises.stat(toPath);
|
|
6383
|
+
await workingCopy.trackFile(args.to, {
|
|
6384
|
+
mtime: stats.mtime,
|
|
6385
|
+
size: stats.size,
|
|
6386
|
+
mode: stats.mode,
|
|
6387
|
+
});
|
|
6388
|
+
|
|
6389
|
+
return {
|
|
6390
|
+
from: args.from,
|
|
6391
|
+
to: args.to,
|
|
6392
|
+
size: stats.size,
|
|
6393
|
+
mode: stats.mode,
|
|
6394
|
+
mtime: stats.mtime,
|
|
6395
|
+
};
|
|
6275
6396
|
},
|
|
6276
6397
|
|
|
6277
6398
|
/**
|
|
6278
|
-
* Move
|
|
6399
|
+
* Move (rebase) a change to a new parent
|
|
6279
6400
|
*
|
|
6280
|
-
* @
|
|
6281
|
-
* @param {
|
|
6401
|
+
* @param {Object} args - Move change arguments
|
|
6402
|
+
* @param {string} args.changeId - Change ID to move (also accepts 'from' or 'source')
|
|
6403
|
+
* @param {string} args.newParent - New parent change ID (also accepts 'to' or 'destination')
|
|
6282
6404
|
* @returns {Promise<Object>} Updated change object
|
|
6283
6405
|
*/
|
|
6284
|
-
async
|
|
6406
|
+
async moveChange(args) {
|
|
6285
6407
|
await graph.load();
|
|
6286
6408
|
|
|
6287
6409
|
// Support multiple parameter names for flexibility
|
|
@@ -6369,110 +6491,62 @@ async function createJJ(options) {
|
|
|
6369
6491
|
},
|
|
6370
6492
|
|
|
6371
6493
|
/**
|
|
6372
|
-
*
|
|
6373
|
-
*
|
|
6374
|
-
* @private
|
|
6375
|
-
* @param {Object} args - Move arguments
|
|
6376
|
-
* @returns {Promise<Object>} Operation result including from, to paths and file stats
|
|
6494
|
+
* @deprecated Use moveFile() for files or moveChange() for history operations
|
|
6495
|
+
* Move operation - delegates to moveFile or moveChange based on arguments
|
|
6377
6496
|
*/
|
|
6378
|
-
async
|
|
6379
|
-
if (!args
|
|
6380
|
-
throw new JJError('INVALID_ARGUMENT', 'Missing or invalid
|
|
6381
|
-
suggestion: '
|
|
6382
|
-
provided: { from: args.from, type: typeof args.from },
|
|
6383
|
-
});
|
|
6384
|
-
}
|
|
6385
|
-
|
|
6386
|
-
if (!args.to || typeof args.to !== 'string') {
|
|
6387
|
-
throw new JJError('INVALID_ARGUMENT', 'Missing or invalid to path', {
|
|
6388
|
-
suggestion: 'Provide a valid destination file path',
|
|
6389
|
-
provided: { to: args.to, type: typeof args.to },
|
|
6497
|
+
async move(args) {
|
|
6498
|
+
if (!args || typeof args !== 'object') {
|
|
6499
|
+
throw new JJError('INVALID_ARGUMENT', 'Missing or invalid arguments', {
|
|
6500
|
+
suggestion: 'Use moveFile({ from, to }) for files or moveChange({ changeId, newParent }) for history',
|
|
6390
6501
|
});
|
|
6391
6502
|
}
|
|
6392
6503
|
|
|
6393
|
-
//
|
|
6394
|
-
|
|
6395
|
-
throw new JJError('INVALID_OPERATION', 'Source and destination paths are the same', {
|
|
6396
|
-
path: args.from,
|
|
6397
|
-
suggestion: 'Specify a different destination path',
|
|
6398
|
-
});
|
|
6399
|
-
}
|
|
6504
|
+
// Detect operation type based on arguments
|
|
6505
|
+
const isHistoryOp = this._detectHistoryOperation(args);
|
|
6400
6506
|
|
|
6401
|
-
|
|
6402
|
-
|
|
6403
|
-
|
|
6404
|
-
|
|
6405
|
-
provided: { from: args.from, to: args.to },
|
|
6406
|
-
});
|
|
6507
|
+
if (isHistoryOp) {
|
|
6508
|
+
return await this.moveChange(args);
|
|
6509
|
+
} else {
|
|
6510
|
+
return await this.moveFile(args);
|
|
6407
6511
|
}
|
|
6512
|
+
},
|
|
6408
6513
|
|
|
6409
|
-
|
|
6410
|
-
|
|
6411
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
6414
|
-
|
|
6514
|
+
/**
|
|
6515
|
+
* Detect if move operation is for history (rebase) or files
|
|
6516
|
+
*
|
|
6517
|
+
* @private
|
|
6518
|
+
* @param {Object} args - Move arguments
|
|
6519
|
+
* @returns {boolean} True if history operation, false if file operation
|
|
6520
|
+
*/
|
|
6521
|
+
_detectHistoryOperation(args) {
|
|
6522
|
+
// Explicit history operation indicators
|
|
6523
|
+
if (args.paths || args.changeId || args.newParent) {
|
|
6524
|
+
return true;
|
|
6415
6525
|
}
|
|
6416
6526
|
|
|
6417
|
-
|
|
6418
|
-
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
} catch (error) {
|
|
6424
|
-
throw new JJError('FILE_NOT_FOUND', `Source file not found: ${args.from}`, {
|
|
6425
|
-
path: args.from,
|
|
6426
|
-
fullPath: fromPath,
|
|
6427
|
-
suggestion: 'Check that the file exists',
|
|
6428
|
-
});
|
|
6429
|
-
}
|
|
6527
|
+
// If only from/to are present, check if they look like change IDs
|
|
6528
|
+
if (args.from && args.to && !args.changeId && !args.newParent && !args.paths) {
|
|
6529
|
+
// Change IDs are 32-character hex strings
|
|
6530
|
+
const changeIdPattern = /^[0-9a-f]{32}$/;
|
|
6531
|
+
const fromLooksLikeChangeId = changeIdPattern.test(args.from);
|
|
6532
|
+
const toLooksLikeChangeId = changeIdPattern.test(args.to);
|
|
6430
6533
|
|
|
6431
|
-
|
|
6432
|
-
|
|
6433
|
-
|
|
6434
|
-
|
|
6435
|
-
|
|
6436
|
-
|
|
6437
|
-
|
|
6438
|
-
|
|
6439
|
-
|
|
6440
|
-
`Failed to create destination directory: ${dirPath}`, {
|
|
6441
|
-
directory: dirPath,
|
|
6442
|
-
originalError: error.message,
|
|
6534
|
+
// If both look like change IDs, it's likely a history operation
|
|
6535
|
+
// However, we err on the side of file operations to be safe
|
|
6536
|
+
// Users should use explicit { changeId, newParent } for clarity
|
|
6537
|
+
if (fromLooksLikeChangeId && toLooksLikeChangeId) {
|
|
6538
|
+
// This is ambiguous - throw a helpful error
|
|
6539
|
+
throw new JJError('AMBIGUOUS_OPERATION',
|
|
6540
|
+
'Ambiguous move operation: arguments look like change IDs', {
|
|
6541
|
+
suggestion: 'Use { changeId, newParent } for history operations or ensure file paths don\'t match change ID pattern',
|
|
6542
|
+
args,
|
|
6443
6543
|
});
|
|
6444
6544
|
}
|
|
6445
|
-
}
|
|
6446
6545
|
|
|
6447
|
-
|
|
6448
|
-
try {
|
|
6449
|
-
await fs.promises.rename(fromPath, toPath);
|
|
6450
|
-
} catch (error) {
|
|
6451
|
-
throw new JJError('FILE_MOVE_FAILED', `Failed to move file: ${error.message}`, {
|
|
6452
|
-
from: args.from,
|
|
6453
|
-
to: args.to,
|
|
6454
|
-
originalError: error.message,
|
|
6455
|
-
});
|
|
6546
|
+
return false; // Treat as file operation
|
|
6456
6547
|
}
|
|
6457
6548
|
|
|
6458
|
-
|
|
6459
|
-
await workingCopy.load();
|
|
6460
|
-
await workingCopy.untrackFile(args.from);
|
|
6461
|
-
const stats = await fs.promises.stat(toPath);
|
|
6462
|
-
await workingCopy.trackFile(args.to, {
|
|
6463
|
-
mtime: stats.mtime,
|
|
6464
|
-
size: stats.size,
|
|
6465
|
-
mode: stats.mode,
|
|
6466
|
-
});
|
|
6467
|
-
|
|
6468
|
-
// Return useful information about the move operation
|
|
6469
|
-
return {
|
|
6470
|
-
from: args.from,
|
|
6471
|
-
to: args.to,
|
|
6472
|
-
size: stats.size,
|
|
6473
|
-
mode: stats.mode,
|
|
6474
|
-
mtime: stats.mtime,
|
|
6475
|
-
};
|
|
6549
|
+
return false;
|
|
6476
6550
|
},
|
|
6477
6551
|
|
|
6478
6552
|
/**
|
|
@@ -7017,7 +7091,8 @@ async function createJJ(options) {
|
|
|
7017
7091
|
* @param {Object} args - Arguments
|
|
7018
7092
|
* @param {string} [args.revset='all()'] - Revset expression to filter changes
|
|
7019
7093
|
* @param {number} [args.limit] - Maximum number of changes to return
|
|
7020
|
-
* @
|
|
7094
|
+
* @param {boolean} [args.count=false] - If true, return count instead of changes (v0.36.0)
|
|
7095
|
+
* @returns {Promise<Array|number>} Array of changes or count if args.count is true
|
|
7021
7096
|
*/
|
|
7022
7097
|
async log(args = {}) {
|
|
7023
7098
|
await graph.load();
|
|
@@ -7026,6 +7101,11 @@ async function createJJ(options) {
|
|
|
7026
7101
|
const revsetExpr = args.revset || 'all()';
|
|
7027
7102
|
const changeIds = await revset.evaluate(revsetExpr);
|
|
7028
7103
|
|
|
7104
|
+
// If count mode, return count early (v0.36.0 feature)
|
|
7105
|
+
if (args.count) {
|
|
7106
|
+
return changeIds.length;
|
|
7107
|
+
}
|
|
7108
|
+
|
|
7029
7109
|
let changes = [];
|
|
7030
7110
|
for (const changeId of changeIds) {
|
|
7031
7111
|
const change = await graph.getChange(changeId);
|
|
@@ -7973,7 +8053,7 @@ async function createJJ(options) {
|
|
|
7973
8053
|
* });
|
|
7974
8054
|
*/
|
|
7975
8055
|
async rebase(args) {
|
|
7976
|
-
return await this.
|
|
8056
|
+
return await this.moveChange(args);
|
|
7977
8057
|
},
|
|
7978
8058
|
|
|
7979
8059
|
/**
|
|
@@ -8046,9 +8126,18 @@ async function createJJ(options) {
|
|
|
8046
8126
|
* @returns {Promise<Object>} The new backout change including changeId, description, and backedOut field
|
|
8047
8127
|
*/
|
|
8048
8128
|
async backout(args) {
|
|
8129
|
+
// Normalize: accept common parameter aliases
|
|
8130
|
+
if (args && args.change && !args.revision) {
|
|
8131
|
+
args = { ...args, revision: args.change };
|
|
8132
|
+
} else if (args && args.changeId && !args.revision) {
|
|
8133
|
+
args = { ...args, revision: args.changeId };
|
|
8134
|
+
} else if (args && args.target && !args.revision) {
|
|
8135
|
+
args = { ...args, revision: args.target };
|
|
8136
|
+
}
|
|
8137
|
+
|
|
8049
8138
|
if (!args || !args.revision) {
|
|
8050
8139
|
throw new JJError('INVALID_ARGUMENT', 'Missing revision argument', {
|
|
8051
|
-
suggestion: 'Provide the change ID to back out: { revision: "abc123..." }',
|
|
8140
|
+
suggestion: 'Provide the change ID to back out: { revision: "abc123..." } (or use change/changeId/target)',
|
|
8052
8141
|
});
|
|
8053
8142
|
}
|
|
8054
8143
|
|
|
@@ -9083,6 +9172,15 @@ async function createJJ(options) {
|
|
|
9083
9172
|
* @param {string[]} [args.paths] - Specific paths to include in first split (full implementation planned for future)
|
|
9084
9173
|
*/
|
|
9085
9174
|
async split(args) {
|
|
9175
|
+
// Normalize: accept common parameter aliases
|
|
9176
|
+
if (args && args.change && !args.changeId) {
|
|
9177
|
+
args = { ...args, changeId: args.change };
|
|
9178
|
+
} else if (args && args.revision && !args.changeId) {
|
|
9179
|
+
args = { ...args, changeId: args.revision };
|
|
9180
|
+
} else if (args && args.target && !args.changeId) {
|
|
9181
|
+
args = { ...args, changeId: args.target };
|
|
9182
|
+
}
|
|
9183
|
+
|
|
9086
9184
|
// Check for interactive mode (not supported)
|
|
9087
9185
|
if (args.interactive) {
|
|
9088
9186
|
throw new JJError(
|
|
@@ -11324,21 +11422,13 @@ async function createJJ(options) {
|
|
|
11324
11422
|
});
|
|
11325
11423
|
}
|
|
11326
11424
|
|
|
11327
|
-
|
|
11328
|
-
|
|
11329
|
-
|
|
11330
|
-
const parts = key.split('.');
|
|
11331
|
-
let value = userConfig.config;
|
|
11332
|
-
|
|
11333
|
-
for (const part of parts) {
|
|
11334
|
-
if (value && typeof value === 'object') {
|
|
11335
|
-
value = value[part];
|
|
11336
|
-
} else {
|
|
11337
|
-
return null;
|
|
11338
|
-
}
|
|
11425
|
+
// Ensure config is loaded at least once
|
|
11426
|
+
if (!userConfig.config) {
|
|
11427
|
+
await userConfig.load();
|
|
11339
11428
|
}
|
|
11340
11429
|
|
|
11341
|
-
|
|
11430
|
+
// Use userConfig.get() to preserve programmatic config
|
|
11431
|
+
return userConfig.get(key);
|
|
11342
11432
|
},
|
|
11343
11433
|
|
|
11344
11434
|
/**
|
|
@@ -11387,6 +11477,87 @@ async function createJJ(options) {
|
|
|
11387
11477
|
await userConfig.load();
|
|
11388
11478
|
return userConfig.config;
|
|
11389
11479
|
},
|
|
11480
|
+
|
|
11481
|
+
/**
|
|
11482
|
+
* Reload configuration (including workspace-specific config)
|
|
11483
|
+
* v0.35.0: Loads workspace-config.json if present
|
|
11484
|
+
* v0.36.0: Accepts optional config object for programmatic configuration
|
|
11485
|
+
*
|
|
11486
|
+
* @param {Object} [opts] - Optional config options
|
|
11487
|
+
* @param {Object} [opts.workspace] - Workspace-specific config to merge (highest priority)
|
|
11488
|
+
* @param {Object} [opts.override] - Config object to merge over loaded config
|
|
11489
|
+
* @returns {Promise<void>}
|
|
11490
|
+
*
|
|
11491
|
+
* @example
|
|
11492
|
+
* // Load from files only
|
|
11493
|
+
* await repo.config.load();
|
|
11494
|
+
*
|
|
11495
|
+
* @example
|
|
11496
|
+
* // Programmatic workspace config (no file I/O needed)
|
|
11497
|
+
* await repo.config.load({
|
|
11498
|
+
* workspace: { user: { email: 'test@example.com' } }
|
|
11499
|
+
* });
|
|
11500
|
+
*
|
|
11501
|
+
* @example
|
|
11502
|
+
* // Override global config programmatically
|
|
11503
|
+
* await repo.config.load({
|
|
11504
|
+
* override: { test: { value: 'override' } }
|
|
11505
|
+
* });
|
|
11506
|
+
*/
|
|
11507
|
+
async load(opts) {
|
|
11508
|
+
await userConfig.load(opts);
|
|
11509
|
+
},
|
|
11510
|
+
},
|
|
11511
|
+
|
|
11512
|
+
/**
|
|
11513
|
+
* Template helper functions (v0.36.0)
|
|
11514
|
+
*
|
|
11515
|
+
* Utilities for displaying and formatting JJ data
|
|
11516
|
+
*/
|
|
11517
|
+
template: {
|
|
11518
|
+
/**
|
|
11519
|
+
* Get files modified in a change
|
|
11520
|
+
*
|
|
11521
|
+
* @param {string} changeId - Change ID
|
|
11522
|
+
* @returns {Promise<string[]>} Array of file paths
|
|
11523
|
+
*/
|
|
11524
|
+
async files(changeId) {
|
|
11525
|
+
await graph.load();
|
|
11526
|
+
const change = await graph.getChange(changeId);
|
|
11527
|
+
if (!change || !change.fileSnapshot) {
|
|
11528
|
+
return [];
|
|
11529
|
+
}
|
|
11530
|
+
return Object.keys(change.fileSnapshot);
|
|
11531
|
+
},
|
|
11532
|
+
|
|
11533
|
+
/**
|
|
11534
|
+
* Join array elements with a separator
|
|
11535
|
+
*
|
|
11536
|
+
* @param {string[]} array - Array of strings
|
|
11537
|
+
* @param {string} separator - Separator string
|
|
11538
|
+
* @returns {string} Joined string
|
|
11539
|
+
*/
|
|
11540
|
+
join(array, separator) {
|
|
11541
|
+
if (!Array.isArray(array)) {
|
|
11542
|
+
return '';
|
|
11543
|
+
}
|
|
11544
|
+
return array.join(separator);
|
|
11545
|
+
},
|
|
11546
|
+
|
|
11547
|
+
/**
|
|
11548
|
+
* Format path for display
|
|
11549
|
+
*
|
|
11550
|
+
* @param {string} path - File path
|
|
11551
|
+
* @returns {string} Formatted path
|
|
11552
|
+
*/
|
|
11553
|
+
format_path(path) {
|
|
11554
|
+
if (!path) {
|
|
11555
|
+
return '';
|
|
11556
|
+
}
|
|
11557
|
+
// Simple formatting: just return the path as-is for now
|
|
11558
|
+
// In real JJ, this might apply colors, truncation, etc.
|
|
11559
|
+
return path;
|
|
11560
|
+
},
|
|
11390
11561
|
},
|
|
11391
11562
|
|
|
11392
11563
|
/**
|