isomorphic-jj 1.1.2 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "isomorphic-jj",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "description": "A pure-JavaScript library that brings Jujutsu (jj) version control semantics to Node.js and browsers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1554,7 +1554,8 @@ export async function createJJ(options) {
1554
1554
  * @param {Object} args - Arguments
1555
1555
  * @param {string} [args.revset='all()'] - Revset expression to filter changes
1556
1556
  * @param {number} [args.limit] - Maximum number of changes to return
1557
- * @returns {Promise<Array>} Array of changes
1557
+ * @param {boolean} [args.count=false] - If true, return count instead of changes (v0.36.0)
1558
+ * @returns {Promise<Array|number>} Array of changes or count if args.count is true
1558
1559
  */
1559
1560
  async log(args = {}) {
1560
1561
  await graph.load();
@@ -1563,6 +1564,11 @@ export async function createJJ(options) {
1563
1564
  const revsetExpr = args.revset || 'all()';
1564
1565
  const changeIds = await revset.evaluate(revsetExpr);
1565
1566
 
1567
+ // If count mode, return count early (v0.36.0 feature)
1568
+ if (args.count) {
1569
+ return changeIds.length;
1570
+ }
1571
+
1566
1572
  let changes = [];
1567
1573
  for (const changeId of changeIds) {
1568
1574
  const change = await graph.getChange(changeId);
@@ -2583,9 +2589,18 @@ export async function createJJ(options) {
2583
2589
  * @returns {Promise<Object>} The new backout change including changeId, description, and backedOut field
2584
2590
  */
2585
2591
  async backout(args) {
2592
+ // Normalize: accept common parameter aliases
2593
+ if (args && args.change && !args.revision) {
2594
+ args = { ...args, revision: args.change };
2595
+ } else if (args && args.changeId && !args.revision) {
2596
+ args = { ...args, revision: args.changeId };
2597
+ } else if (args && args.target && !args.revision) {
2598
+ args = { ...args, revision: args.target };
2599
+ }
2600
+
2586
2601
  if (!args || !args.revision) {
2587
2602
  throw new JJError('INVALID_ARGUMENT', 'Missing revision argument', {
2588
- suggestion: 'Provide the change ID to back out: { revision: "abc123..." }',
2603
+ suggestion: 'Provide the change ID to back out: { revision: "abc123..." } (or use change/changeId/target)',
2589
2604
  });
2590
2605
  }
2591
2606
 
@@ -3620,6 +3635,15 @@ export async function createJJ(options) {
3620
3635
  * @param {string[]} [args.paths] - Specific paths to include in first split (full implementation planned for future)
3621
3636
  */
3622
3637
  async split(args) {
3638
+ // Normalize: accept common parameter aliases
3639
+ if (args && args.change && !args.changeId) {
3640
+ args = { ...args, changeId: args.change };
3641
+ } else if (args && args.revision && !args.changeId) {
3642
+ args = { ...args, changeId: args.revision };
3643
+ } else if (args && args.target && !args.changeId) {
3644
+ args = { ...args, changeId: args.target };
3645
+ }
3646
+
3623
3647
  // Check for interactive mode (not supported)
3624
3648
  if (args.interactive) {
3625
3649
  throw new JJError(
@@ -5331,9 +5355,21 @@ export async function createJJ(options) {
5331
5355
  * @returns {Promise<Object>} Bookmark info
5332
5356
  */
5333
5357
  async set(args) {
5358
+ // Normalize: accept common parameter aliases
5359
+ if (args) {
5360
+ if (args.target && !args.changeId) {
5361
+ args = { ...args, changeId: args.target };
5362
+ } else if (args.change && !args.changeId) {
5363
+ args = { ...args, changeId: args.change };
5364
+ } else if (args.revision && !args.changeId) {
5365
+ args = { ...args, changeId: args.revision };
5366
+ }
5367
+ }
5368
+
5334
5369
  if (!args || !args.name || !args.changeId) {
5335
5370
  throw new JJError('INVALID_ARGUMENT', 'Missing name or changeId', {
5336
- suggestion: 'Provide both: { name: "main", changeId: "abc123..." }',
5371
+ args,
5372
+ suggestion: 'Provide both: { name: "main", changeId: "abc123..." } (or use target/change/revision instead of changeId)',
5337
5373
  });
5338
5374
  }
5339
5375
 
@@ -5369,6 +5405,17 @@ export async function createJJ(options) {
5369
5405
  * @returns {Promise<Object>} Bookmark info
5370
5406
  */
5371
5407
  async create(args) {
5408
+ // Normalize: accept common parameter aliases
5409
+ if (args) {
5410
+ if (args.target && !args.changeId) {
5411
+ args = { ...args, changeId: args.target };
5412
+ } else if (args.change && !args.changeId) {
5413
+ args = { ...args, changeId: args.change };
5414
+ } else if (args.revision && !args.changeId) {
5415
+ args = { ...args, changeId: args.revision };
5416
+ }
5417
+ }
5418
+
5372
5419
  if (!args || !args.name) {
5373
5420
  throw new JJError('INVALID_ARGUMENT', 'Missing name argument', {
5374
5421
  suggestion: 'Provide: { name: "main", changeId: "abc123..." } or { name: "main" } for working copy',
@@ -5421,9 +5468,23 @@ export async function createJJ(options) {
5421
5468
  * @returns {Promise<Object>} Move result
5422
5469
  */
5423
5470
  async move(args) {
5471
+ // Normalize: accept common parameter aliases
5472
+ if (args) {
5473
+ if (args.target && !args.to) {
5474
+ args = { ...args, to: args.target };
5475
+ } else if (args.changeId && !args.to) {
5476
+ args = { ...args, to: args.changeId };
5477
+ } else if (args.change && !args.to) {
5478
+ args = { ...args, to: args.change };
5479
+ } else if (args.revision && !args.to) {
5480
+ args = { ...args, to: args.revision };
5481
+ }
5482
+ }
5483
+
5424
5484
  if (!args || !args.name || !args.to) {
5425
5485
  throw new JJError('INVALID_ARGUMENT', 'Missing name or to', {
5426
- suggestion: 'Provide both: { name: "main", to: "abc123..." }',
5486
+ args,
5487
+ suggestion: 'Provide both: { name: "main", to: "abc123..." } (or use target/changeId/change/revision instead of to)',
5427
5488
  });
5428
5489
  }
5429
5490
 
@@ -5824,21 +5885,13 @@ export async function createJJ(options) {
5824
5885
  });
5825
5886
  }
5826
5887
 
5827
- await userConfig.load();
5828
-
5829
- // Handle nested keys (e.g., 'user.name')
5830
- const parts = key.split('.');
5831
- let value = userConfig.config;
5832
-
5833
- for (const part of parts) {
5834
- if (value && typeof value === 'object') {
5835
- value = value[part];
5836
- } else {
5837
- return null;
5838
- }
5888
+ // Ensure config is loaded at least once
5889
+ if (!userConfig.config) {
5890
+ await userConfig.load();
5839
5891
  }
5840
5892
 
5841
- return value;
5893
+ // Use userConfig.get() to preserve programmatic config
5894
+ return userConfig.get(key);
5842
5895
  },
5843
5896
 
5844
5897
  /**
@@ -5887,6 +5940,87 @@ export async function createJJ(options) {
5887
5940
  await userConfig.load();
5888
5941
  return userConfig.config;
5889
5942
  },
5943
+
5944
+ /**
5945
+ * Reload configuration (including workspace-specific config)
5946
+ * v0.35.0: Loads workspace-config.json if present
5947
+ * v0.36.0: Accepts optional config object for programmatic configuration
5948
+ *
5949
+ * @param {Object} [opts] - Optional config options
5950
+ * @param {Object} [opts.workspace] - Workspace-specific config to merge (highest priority)
5951
+ * @param {Object} [opts.override] - Config object to merge over loaded config
5952
+ * @returns {Promise<void>}
5953
+ *
5954
+ * @example
5955
+ * // Load from files only
5956
+ * await repo.config.load();
5957
+ *
5958
+ * @example
5959
+ * // Programmatic workspace config (no file I/O needed)
5960
+ * await repo.config.load({
5961
+ * workspace: { user: { email: 'test@example.com' } }
5962
+ * });
5963
+ *
5964
+ * @example
5965
+ * // Override global config programmatically
5966
+ * await repo.config.load({
5967
+ * override: { test: { value: 'override' } }
5968
+ * });
5969
+ */
5970
+ async load(opts) {
5971
+ await userConfig.load(opts);
5972
+ },
5973
+ },
5974
+
5975
+ /**
5976
+ * Template helper functions (v0.36.0)
5977
+ *
5978
+ * Utilities for displaying and formatting JJ data
5979
+ */
5980
+ template: {
5981
+ /**
5982
+ * Get files modified in a change
5983
+ *
5984
+ * @param {string} changeId - Change ID
5985
+ * @returns {Promise<string[]>} Array of file paths
5986
+ */
5987
+ async files(changeId) {
5988
+ await graph.load();
5989
+ const change = await graph.getChange(changeId);
5990
+ if (!change || !change.fileSnapshot) {
5991
+ return [];
5992
+ }
5993
+ return Object.keys(change.fileSnapshot);
5994
+ },
5995
+
5996
+ /**
5997
+ * Join array elements with a separator
5998
+ *
5999
+ * @param {string[]} array - Array of strings
6000
+ * @param {string} separator - Separator string
6001
+ * @returns {string} Joined string
6002
+ */
6003
+ join(array, separator) {
6004
+ if (!Array.isArray(array)) {
6005
+ return '';
6006
+ }
6007
+ return array.join(separator);
6008
+ },
6009
+
6010
+ /**
6011
+ * Format path for display
6012
+ *
6013
+ * @param {string} path - File path
6014
+ * @returns {string} Formatted path
6015
+ */
6016
+ format_path(path) {
6017
+ if (!path) {
6018
+ return '';
6019
+ }
6020
+ // Simple formatting: just return the path as-is for now
6021
+ // In real JJ, this might apply colors, truncation, etc.
6022
+ return path;
6023
+ },
5890
6024
  },
5891
6025
 
5892
6026
  /**
@@ -15,6 +15,7 @@ export class BookmarkStore {
15
15
  this.storage = storage;
16
16
  this.local = new Map();
17
17
  this.remote = new Map(); // remote → (bookmark → changeId)
18
+ this.tracking = {}; // bookmark → { remote, ref } tracking info (v0.35.0)
18
19
  }
19
20
 
20
21
  /**
@@ -57,6 +58,7 @@ export class BookmarkStore {
57
58
 
58
59
  this.local.clear();
59
60
  this.remote.clear();
61
+ this.tracking = data.tracked || {}; // Load tracking info (v0.35.0)
60
62
 
61
63
  // Load local bookmarks
62
64
  for (const [name, target] of Object.entries(data.local)) {
@@ -94,7 +96,7 @@ export class BookmarkStore {
94
96
  version: 1,
95
97
  local,
96
98
  remote,
97
- tracked: {},
99
+ tracked: this.tracking || {}, // Save tracking info (v0.35.0)
98
100
  });
99
101
  }
100
102
 
@@ -207,13 +209,23 @@ export class BookmarkStore {
207
209
  async list() {
208
210
  const result = [];
209
211
 
210
- // Add local bookmarks
212
+ // Add local bookmarks with tracking info (v0.35.0)
211
213
  for (const [name, target] of this.local.entries()) {
212
- result.push({
214
+ const bookmark = {
213
215
  name,
214
216
  changeId: target,
215
217
  remote: null,
216
- });
218
+ };
219
+
220
+ // Include tracking info if available
221
+ if (this.tracking && this.tracking[name]) {
222
+ bookmark.tracking = {
223
+ remote: this.tracking[name].remote,
224
+ ref: this.tracking[name].remoteName || name,
225
+ };
226
+ }
227
+
228
+ result.push(bookmark);
217
229
  }
218
230
 
219
231
  // Add remote bookmarks
@@ -12,11 +12,17 @@ import { fileURLToPath } from 'url';
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = path.dirname(__filename);
14
14
 
15
+ // Determine proto path based on whether we're running from dist or src
16
+ const inDist = __dirname.includes('/dist') || __dirname.includes('\\dist');
17
+ const protoDir = inDist
18
+ ? path.join(__dirname, '..', 'src', 'protos')
19
+ : path.join(__dirname, '..', 'protos');
20
+
15
21
  export class JJCheckout {
16
22
  constructor(fs, dir) {
17
23
  this.fs = fs;
18
24
  this.dir = dir;
19
- this.protoPath = path.join(__dirname, '..', 'protos', 'local_working_copy.proto');
25
+ this.protoPath = path.join(protoDir, 'local_working_copy.proto');
20
26
  }
21
27
 
22
28
  /**
@@ -12,11 +12,17 @@ import { fileURLToPath } from 'url';
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = path.dirname(__filename);
14
14
 
15
+ // Determine proto path based on whether we're running from dist or src
16
+ const inDist = __dirname.includes('/dist') || __dirname.includes('\\dist');
17
+ const protoDir = inDist
18
+ ? path.join(__dirname, '..', 'src', 'protos')
19
+ : path.join(__dirname, '..', 'protos');
20
+
15
21
  export class JJOperationStore {
16
22
  constructor(fs, dir) {
17
23
  this.fs = fs;
18
24
  this.dir = dir;
19
- this.protoPath = path.join(__dirname, '..', 'protos', 'simple_op_store.proto');
25
+ this.protoPath = path.join(protoDir, 'simple_op_store.proto');
20
26
  }
21
27
 
22
28
  /**
@@ -12,11 +12,17 @@ import { fileURLToPath } from 'url';
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = path.dirname(__filename);
14
14
 
15
+ // Determine proto path based on whether we're running from dist or src
16
+ const inDist = __dirname.includes('/dist') || __dirname.includes('\\dist');
17
+ const protoDir = inDist
18
+ ? path.join(__dirname, '..', 'src', 'protos')
19
+ : path.join(__dirname, '..', 'protos');
20
+
15
21
  export class JJTreeState {
16
22
  constructor(fs, dir) {
17
23
  this.fs = fs;
18
24
  this.dir = dir;
19
- this.protoPath = path.join(__dirname, '..', 'protos', 'local_working_copy.proto');
25
+ this.protoPath = path.join(protoDir, 'local_working_copy.proto');
20
26
  }
21
27
 
22
28
  /**
@@ -12,11 +12,17 @@ import { fileURLToPath } from 'url';
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = path.dirname(__filename);
14
14
 
15
+ // Determine proto path based on whether we're running from dist or src
16
+ const inDist = __dirname.includes('/dist') || __dirname.includes('\\dist');
17
+ const protoDir = inDist
18
+ ? path.join(__dirname, '..', 'src', 'protos')
19
+ : path.join(__dirname, '..', 'protos');
20
+
15
21
  export class JJViewStore {
16
22
  constructor(fs, dir) {
17
23
  this.fs = fs;
18
24
  this.dir = dir;
19
- this.protoPath = path.join(__dirname, '..', 'protos', 'simple_op_store.proto');
25
+ this.protoPath = path.join(protoDir, 'simple_op_store.proto');
20
26
  }
21
27
 
22
28
  /**
@@ -129,6 +129,24 @@ export class RevsetEngine {
129
129
  }
130
130
  }
131
131
 
132
+ // v0.36.0: visible() - non-abandoned changes
133
+ if (trimmed === 'visible()') {
134
+ await this.graph.load();
135
+ const all = this.graph.getAll();
136
+ return all
137
+ .filter(c => !c.abandoned)
138
+ .map(c => c.changeId);
139
+ }
140
+
141
+ // v0.36.0: hidden() - abandoned changes
142
+ if (trimmed === 'hidden()') {
143
+ await this.graph.load();
144
+ const all = this.graph.getAll();
145
+ return all
146
+ .filter(c => c.abandoned === true)
147
+ .map(c => c.changeId);
148
+ }
149
+
132
150
  // ancestors(changeId) - all ancestors including the change itself
133
151
  const ancestorsMatch = trimmed.match(/^ancestors\(([0-9a-f]{32})\)$/);
134
152
  if (ancestorsMatch) {
@@ -37,9 +37,15 @@ export class UserConfig {
37
37
  }
38
38
 
39
39
  /**
40
- * Load config from storage
40
+ * Load config from storage and/or programmatic object
41
+ * v0.35.0: Also loads workspace-specific config from workspace-config.json
42
+ * v0.36.0: Accepts optional config object for programmatic configuration
43
+ *
44
+ * @param {Object} [opts] - Optional config options
45
+ * @param {Object} [opts.workspace] - Workspace-specific config to merge (highest priority)
46
+ * @param {Object} [opts.override] - Config object to merge over loaded config
41
47
  */
42
- async load() {
48
+ async load(opts = {}) {
43
49
  try {
44
50
  const data = await this.storage.read('config.json');
45
51
  if (data) {
@@ -49,6 +55,45 @@ export class UserConfig {
49
55
  // No config file yet, use defaults
50
56
  await this.init();
51
57
  }
58
+
59
+ // Merge programmatic override config if provided
60
+ if (opts.override) {
61
+ this.config = this._deepMerge(this.config, opts.override);
62
+ }
63
+
64
+ // Load workspace-specific config from file (v0.35.0)
65
+ try {
66
+ const workspaceData = await this.storage.read('workspace-config.json');
67
+ if (workspaceData) {
68
+ // Merge workspace config over global config (workspace takes precedence)
69
+ this.config = this._deepMerge(this.config, workspaceData);
70
+ }
71
+ } catch (error) {
72
+ // No workspace config, that's fine
73
+ }
74
+
75
+ // Merge programmatic workspace config if provided (highest priority)
76
+ if (opts.workspace) {
77
+ this.config = this._deepMerge(this.config, opts.workspace);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Deep merge two config objects (workspace overrides global)
83
+ * @private
84
+ */
85
+ _deepMerge(target, source) {
86
+ const result = { ...target };
87
+
88
+ for (const key in source) {
89
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
90
+ result[key] = this._deepMerge(target[key] || {}, source[key]);
91
+ } else {
92
+ result[key] = source[key];
93
+ }
94
+ }
95
+
96
+ return result;
52
97
  }
53
98
 
54
99
  /**