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