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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "isomorphic-jj",
3
- "version": "1.1.3",
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(
@@ -5861,21 +5885,13 @@ export async function createJJ(options) {
5861
5885
  });
5862
5886
  }
5863
5887
 
5864
- await userConfig.load();
5865
-
5866
- // Handle nested keys (e.g., 'user.name')
5867
- const parts = key.split('.');
5868
- let value = userConfig.config;
5869
-
5870
- for (const part of parts) {
5871
- if (value && typeof value === 'object') {
5872
- value = value[part];
5873
- } else {
5874
- return null;
5875
- }
5888
+ // Ensure config is loaded at least once
5889
+ if (!userConfig.config) {
5890
+ await userConfig.load();
5876
5891
  }
5877
5892
 
5878
- return value;
5893
+ // Use userConfig.get() to preserve programmatic config
5894
+ return userConfig.get(key);
5879
5895
  },
5880
5896
 
5881
5897
  /**
@@ -5924,6 +5940,87 @@ export async function createJJ(options) {
5924
5940
  await userConfig.load();
5925
5941
  return userConfig.config;
5926
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
+ },
5927
6024
  },
5928
6025
 
5929
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
@@ -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
  /**