isomorphic-jj 1.1.3 → 1.2.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/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
- result.push({
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
  /**
@@ -7014,7 +7089,8 @@ async function createJJ(options) {
7014
7089
  * @param {Object} args - Arguments
7015
7090
  * @param {string} [args.revset='all()'] - Revset expression to filter changes
7016
7091
  * @param {number} [args.limit] - Maximum number of changes to return
7017
- * @returns {Promise<Array>} Array of changes
7092
+ * @param {boolean} [args.count=false] - If true, return count instead of changes (v0.36.0)
7093
+ * @returns {Promise<Array|number>} Array of changes or count if args.count is true
7018
7094
  */
7019
7095
  async log(args = {}) {
7020
7096
  await graph.load();
@@ -7023,6 +7099,11 @@ async function createJJ(options) {
7023
7099
  const revsetExpr = args.revset || 'all()';
7024
7100
  const changeIds = await revset.evaluate(revsetExpr);
7025
7101
 
7102
+ // If count mode, return count early (v0.36.0 feature)
7103
+ if (args.count) {
7104
+ return changeIds.length;
7105
+ }
7106
+
7026
7107
  let changes = [];
7027
7108
  for (const changeId of changeIds) {
7028
7109
  const change = await graph.getChange(changeId);
@@ -8043,9 +8124,18 @@ async function createJJ(options) {
8043
8124
  * @returns {Promise<Object>} The new backout change including changeId, description, and backedOut field
8044
8125
  */
8045
8126
  async backout(args) {
8127
+ // Normalize: accept common parameter aliases
8128
+ if (args && args.change && !args.revision) {
8129
+ args = { ...args, revision: args.change };
8130
+ } else if (args && args.changeId && !args.revision) {
8131
+ args = { ...args, revision: args.changeId };
8132
+ } else if (args && args.target && !args.revision) {
8133
+ args = { ...args, revision: args.target };
8134
+ }
8135
+
8046
8136
  if (!args || !args.revision) {
8047
8137
  throw new JJError('INVALID_ARGUMENT', 'Missing revision argument', {
8048
- suggestion: 'Provide the change ID to back out: { revision: "abc123..." }',
8138
+ suggestion: 'Provide the change ID to back out: { revision: "abc123..." } (or use change/changeId/target)',
8049
8139
  });
8050
8140
  }
8051
8141
 
@@ -9080,6 +9170,15 @@ async function createJJ(options) {
9080
9170
  * @param {string[]} [args.paths] - Specific paths to include in first split (full implementation planned for future)
9081
9171
  */
9082
9172
  async split(args) {
9173
+ // Normalize: accept common parameter aliases
9174
+ if (args && args.change && !args.changeId) {
9175
+ args = { ...args, changeId: args.change };
9176
+ } else if (args && args.revision && !args.changeId) {
9177
+ args = { ...args, changeId: args.revision };
9178
+ } else if (args && args.target && !args.changeId) {
9179
+ args = { ...args, changeId: args.target };
9180
+ }
9181
+
9083
9182
  // Check for interactive mode (not supported)
9084
9183
  if (args.interactive) {
9085
9184
  throw new JJError(
@@ -11321,21 +11420,13 @@ async function createJJ(options) {
11321
11420
  });
11322
11421
  }
11323
11422
 
11324
- await userConfig.load();
11325
-
11326
- // Handle nested keys (e.g., 'user.name')
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
- }
11423
+ // Ensure config is loaded at least once
11424
+ if (!userConfig.config) {
11425
+ await userConfig.load();
11336
11426
  }
11337
11427
 
11338
- return value;
11428
+ // Use userConfig.get() to preserve programmatic config
11429
+ return userConfig.get(key);
11339
11430
  },
11340
11431
 
11341
11432
  /**
@@ -11384,6 +11475,87 @@ async function createJJ(options) {
11384
11475
  await userConfig.load();
11385
11476
  return userConfig.config;
11386
11477
  },
11478
+
11479
+ /**
11480
+ * Reload configuration (including workspace-specific config)
11481
+ * v0.35.0: Loads workspace-config.json if present
11482
+ * v0.36.0: Accepts optional config object for programmatic configuration
11483
+ *
11484
+ * @param {Object} [opts] - Optional config options
11485
+ * @param {Object} [opts.workspace] - Workspace-specific config to merge (highest priority)
11486
+ * @param {Object} [opts.override] - Config object to merge over loaded config
11487
+ * @returns {Promise<void>}
11488
+ *
11489
+ * @example
11490
+ * // Load from files only
11491
+ * await repo.config.load();
11492
+ *
11493
+ * @example
11494
+ * // Programmatic workspace config (no file I/O needed)
11495
+ * await repo.config.load({
11496
+ * workspace: { user: { email: 'test@example.com' } }
11497
+ * });
11498
+ *
11499
+ * @example
11500
+ * // Override global config programmatically
11501
+ * await repo.config.load({
11502
+ * override: { test: { value: 'override' } }
11503
+ * });
11504
+ */
11505
+ async load(opts) {
11506
+ await userConfig.load(opts);
11507
+ },
11508
+ },
11509
+
11510
+ /**
11511
+ * Template helper functions (v0.36.0)
11512
+ *
11513
+ * Utilities for displaying and formatting JJ data
11514
+ */
11515
+ template: {
11516
+ /**
11517
+ * Get files modified in a change
11518
+ *
11519
+ * @param {string} changeId - Change ID
11520
+ * @returns {Promise<string[]>} Array of file paths
11521
+ */
11522
+ async files(changeId) {
11523
+ await graph.load();
11524
+ const change = await graph.getChange(changeId);
11525
+ if (!change || !change.fileSnapshot) {
11526
+ return [];
11527
+ }
11528
+ return Object.keys(change.fileSnapshot);
11529
+ },
11530
+
11531
+ /**
11532
+ * Join array elements with a separator
11533
+ *
11534
+ * @param {string[]} array - Array of strings
11535
+ * @param {string} separator - Separator string
11536
+ * @returns {string} Joined string
11537
+ */
11538
+ join(array, separator) {
11539
+ if (!Array.isArray(array)) {
11540
+ return '';
11541
+ }
11542
+ return array.join(separator);
11543
+ },
11544
+
11545
+ /**
11546
+ * Format path for display
11547
+ *
11548
+ * @param {string} path - File path
11549
+ * @returns {string} Formatted path
11550
+ */
11551
+ format_path(path) {
11552
+ if (!path) {
11553
+ return '';
11554
+ }
11555
+ // Simple formatting: just return the path as-is for now
11556
+ // In real JJ, this might apply colors, truncation, etc.
11557
+ return path;
11558
+ },
11387
11559
  },
11388
11560
 
11389
11561
  /**