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/README.md +48 -0
- package/dist/index.cjs +193 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +193 -21
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/api/repository.js +112 -15
- 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/README.md
CHANGED
|
@@ -202,6 +202,54 @@ await jj.amend();
|
|
|
202
202
|
await jj.undo();
|
|
203
203
|
```
|
|
204
204
|
|
|
205
|
+
### Understanding Working Copy vs Parent
|
|
206
|
+
|
|
207
|
+
A common source of confusion: **the working copy and its parent are different changes**.
|
|
208
|
+
|
|
209
|
+
```javascript
|
|
210
|
+
// After init, you have a working copy change (with changeId)
|
|
211
|
+
const status1 = await jj.status();
|
|
212
|
+
console.log(status1.workingCopy.changeId); // e.g., "abc123..."
|
|
213
|
+
console.log(status1.workingCopy.description); // "(no description set)"
|
|
214
|
+
|
|
215
|
+
// When you describe(), you're updating the PARENT change
|
|
216
|
+
await jj.describe({ message: 'Add feature' });
|
|
217
|
+
|
|
218
|
+
// The working copy changeId is STILL the same!
|
|
219
|
+
const status2 = await jj.status();
|
|
220
|
+
console.log(status2.workingCopy.changeId); // SAME: "abc123..."
|
|
221
|
+
console.log(status2.workingCopy.description); // UPDATED: "Add feature"
|
|
222
|
+
|
|
223
|
+
// When you new(), you create a NEW working copy
|
|
224
|
+
await jj.new({ message: 'Next change' });
|
|
225
|
+
|
|
226
|
+
const status3 = await jj.status();
|
|
227
|
+
console.log(status3.workingCopy.changeId); // DIFFERENT: "def456..."
|
|
228
|
+
console.log(status3.workingCopy.description); // "Next change"
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
**Key points:**
|
|
232
|
+
- **Working copy IS a change** with its own `changeId` (always `@` in revsets)
|
|
233
|
+
- **`describe()`** updates the parent's description, not the working copy's changeId
|
|
234
|
+
- **`status()`** returns the working copy changeId (which may or may not have a description)
|
|
235
|
+
- **`new()`** creates a new working copy change and makes the previous one the parent
|
|
236
|
+
- **Reading metadata immediately after write** shows working copy state, not parent state
|
|
237
|
+
|
|
238
|
+
**Common pattern:**
|
|
239
|
+
```javascript
|
|
240
|
+
// 1. Edit files
|
|
241
|
+
await jj.write({ path: 'file.js', data: 'content' });
|
|
242
|
+
|
|
243
|
+
// 2. Describe the change (updates parent)
|
|
244
|
+
await jj.describe({ message: 'Add file' });
|
|
245
|
+
|
|
246
|
+
// 3. Create new working copy for next change
|
|
247
|
+
await jj.new({ message: 'Next feature' });
|
|
248
|
+
|
|
249
|
+
// Or use commit() to combine describe() + new()
|
|
250
|
+
await jj.commit({ message: 'Add file', nextMessage: 'Next feature' });
|
|
251
|
+
```
|
|
252
|
+
|
|
205
253
|
---
|
|
206
254
|
|
|
207
255
|
## Features
|
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
|
/**
|
|
@@ -7017,7 +7092,8 @@ async function createJJ(options) {
|
|
|
7017
7092
|
* @param {Object} args - Arguments
|
|
7018
7093
|
* @param {string} [args.revset='all()'] - Revset expression to filter changes
|
|
7019
7094
|
* @param {number} [args.limit] - Maximum number of changes to return
|
|
7020
|
-
* @
|
|
7095
|
+
* @param {boolean} [args.count=false] - If true, return count instead of changes (v0.36.0)
|
|
7096
|
+
* @returns {Promise<Array|number>} Array of changes or count if args.count is true
|
|
7021
7097
|
*/
|
|
7022
7098
|
async log(args = {}) {
|
|
7023
7099
|
await graph.load();
|
|
@@ -7026,6 +7102,11 @@ async function createJJ(options) {
|
|
|
7026
7102
|
const revsetExpr = args.revset || 'all()';
|
|
7027
7103
|
const changeIds = await revset.evaluate(revsetExpr);
|
|
7028
7104
|
|
|
7105
|
+
// If count mode, return count early (v0.36.0 feature)
|
|
7106
|
+
if (args.count) {
|
|
7107
|
+
return changeIds.length;
|
|
7108
|
+
}
|
|
7109
|
+
|
|
7029
7110
|
let changes = [];
|
|
7030
7111
|
for (const changeId of changeIds) {
|
|
7031
7112
|
const change = await graph.getChange(changeId);
|
|
@@ -8046,9 +8127,18 @@ async function createJJ(options) {
|
|
|
8046
8127
|
* @returns {Promise<Object>} The new backout change including changeId, description, and backedOut field
|
|
8047
8128
|
*/
|
|
8048
8129
|
async backout(args) {
|
|
8130
|
+
// Normalize: accept common parameter aliases
|
|
8131
|
+
if (args && args.change && !args.revision) {
|
|
8132
|
+
args = { ...args, revision: args.change };
|
|
8133
|
+
} else if (args && args.changeId && !args.revision) {
|
|
8134
|
+
args = { ...args, revision: args.changeId };
|
|
8135
|
+
} else if (args && args.target && !args.revision) {
|
|
8136
|
+
args = { ...args, revision: args.target };
|
|
8137
|
+
}
|
|
8138
|
+
|
|
8049
8139
|
if (!args || !args.revision) {
|
|
8050
8140
|
throw new JJError('INVALID_ARGUMENT', 'Missing revision argument', {
|
|
8051
|
-
suggestion: 'Provide the change ID to back out: { revision: "abc123..." }',
|
|
8141
|
+
suggestion: 'Provide the change ID to back out: { revision: "abc123..." } (or use change/changeId/target)',
|
|
8052
8142
|
});
|
|
8053
8143
|
}
|
|
8054
8144
|
|
|
@@ -9083,6 +9173,15 @@ async function createJJ(options) {
|
|
|
9083
9173
|
* @param {string[]} [args.paths] - Specific paths to include in first split (full implementation planned for future)
|
|
9084
9174
|
*/
|
|
9085
9175
|
async split(args) {
|
|
9176
|
+
// Normalize: accept common parameter aliases
|
|
9177
|
+
if (args && args.change && !args.changeId) {
|
|
9178
|
+
args = { ...args, changeId: args.change };
|
|
9179
|
+
} else if (args && args.revision && !args.changeId) {
|
|
9180
|
+
args = { ...args, changeId: args.revision };
|
|
9181
|
+
} else if (args && args.target && !args.changeId) {
|
|
9182
|
+
args = { ...args, changeId: args.target };
|
|
9183
|
+
}
|
|
9184
|
+
|
|
9086
9185
|
// Check for interactive mode (not supported)
|
|
9087
9186
|
if (args.interactive) {
|
|
9088
9187
|
throw new JJError(
|
|
@@ -11324,21 +11423,13 @@ async function createJJ(options) {
|
|
|
11324
11423
|
});
|
|
11325
11424
|
}
|
|
11326
11425
|
|
|
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
|
-
}
|
|
11426
|
+
// Ensure config is loaded at least once
|
|
11427
|
+
if (!userConfig.config) {
|
|
11428
|
+
await userConfig.load();
|
|
11339
11429
|
}
|
|
11340
11430
|
|
|
11341
|
-
|
|
11431
|
+
// Use userConfig.get() to preserve programmatic config
|
|
11432
|
+
return userConfig.get(key);
|
|
11342
11433
|
},
|
|
11343
11434
|
|
|
11344
11435
|
/**
|
|
@@ -11387,6 +11478,87 @@ async function createJJ(options) {
|
|
|
11387
11478
|
await userConfig.load();
|
|
11388
11479
|
return userConfig.config;
|
|
11389
11480
|
},
|
|
11481
|
+
|
|
11482
|
+
/**
|
|
11483
|
+
* Reload configuration (including workspace-specific config)
|
|
11484
|
+
* v0.35.0: Loads workspace-config.json if present
|
|
11485
|
+
* v0.36.0: Accepts optional config object for programmatic configuration
|
|
11486
|
+
*
|
|
11487
|
+
* @param {Object} [opts] - Optional config options
|
|
11488
|
+
* @param {Object} [opts.workspace] - Workspace-specific config to merge (highest priority)
|
|
11489
|
+
* @param {Object} [opts.override] - Config object to merge over loaded config
|
|
11490
|
+
* @returns {Promise<void>}
|
|
11491
|
+
*
|
|
11492
|
+
* @example
|
|
11493
|
+
* // Load from files only
|
|
11494
|
+
* await repo.config.load();
|
|
11495
|
+
*
|
|
11496
|
+
* @example
|
|
11497
|
+
* // Programmatic workspace config (no file I/O needed)
|
|
11498
|
+
* await repo.config.load({
|
|
11499
|
+
* workspace: { user: { email: 'test@example.com' } }
|
|
11500
|
+
* });
|
|
11501
|
+
*
|
|
11502
|
+
* @example
|
|
11503
|
+
* // Override global config programmatically
|
|
11504
|
+
* await repo.config.load({
|
|
11505
|
+
* override: { test: { value: 'override' } }
|
|
11506
|
+
* });
|
|
11507
|
+
*/
|
|
11508
|
+
async load(opts) {
|
|
11509
|
+
await userConfig.load(opts);
|
|
11510
|
+
},
|
|
11511
|
+
},
|
|
11512
|
+
|
|
11513
|
+
/**
|
|
11514
|
+
* Template helper functions (v0.36.0)
|
|
11515
|
+
*
|
|
11516
|
+
* Utilities for displaying and formatting JJ data
|
|
11517
|
+
*/
|
|
11518
|
+
template: {
|
|
11519
|
+
/**
|
|
11520
|
+
* Get files modified in a change
|
|
11521
|
+
*
|
|
11522
|
+
* @param {string} changeId - Change ID
|
|
11523
|
+
* @returns {Promise<string[]>} Array of file paths
|
|
11524
|
+
*/
|
|
11525
|
+
async files(changeId) {
|
|
11526
|
+
await graph.load();
|
|
11527
|
+
const change = await graph.getChange(changeId);
|
|
11528
|
+
if (!change || !change.fileSnapshot) {
|
|
11529
|
+
return [];
|
|
11530
|
+
}
|
|
11531
|
+
return Object.keys(change.fileSnapshot);
|
|
11532
|
+
},
|
|
11533
|
+
|
|
11534
|
+
/**
|
|
11535
|
+
* Join array elements with a separator
|
|
11536
|
+
*
|
|
11537
|
+
* @param {string[]} array - Array of strings
|
|
11538
|
+
* @param {string} separator - Separator string
|
|
11539
|
+
* @returns {string} Joined string
|
|
11540
|
+
*/
|
|
11541
|
+
join(array, separator) {
|
|
11542
|
+
if (!Array.isArray(array)) {
|
|
11543
|
+
return '';
|
|
11544
|
+
}
|
|
11545
|
+
return array.join(separator);
|
|
11546
|
+
},
|
|
11547
|
+
|
|
11548
|
+
/**
|
|
11549
|
+
* Format path for display
|
|
11550
|
+
*
|
|
11551
|
+
* @param {string} path - File path
|
|
11552
|
+
* @returns {string} Formatted path
|
|
11553
|
+
*/
|
|
11554
|
+
format_path(path) {
|
|
11555
|
+
if (!path) {
|
|
11556
|
+
return '';
|
|
11557
|
+
}
|
|
11558
|
+
// Simple formatting: just return the path as-is for now
|
|
11559
|
+
// In real JJ, this might apply colors, truncation, etc.
|
|
11560
|
+
return path;
|
|
11561
|
+
},
|
|
11390
11562
|
},
|
|
11391
11563
|
|
|
11392
11564
|
/**
|