isomorphic-jj 1.13.0 → 1.14.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/CHANGELOG.md +32 -0
- package/README.md +2 -1
- package/dist/index.cjs +148 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +148 -7
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
- package/src/api/repository.js +82 -4
- package/src/core/operation-log.js +18 -0
- package/src/core/revset-engine.js +48 -3
- package/src/types.d.ts +77 -33
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.14.0 — 2026-10-09 — describe()/metaedit() honor changeId; operations.restore() restores the view
|
|
4
|
+
|
|
5
|
+
Fixes [#55](https://github.com/johnhenry/isomorphic-jj/issues/55) and
|
|
6
|
+
[#56](https://github.com/johnhenry/isomorphic-jj/issues/56). Both change
|
|
7
|
+
observable behavior, hence a minor bump.
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- **`describe()` and `metaedit()` ignored `changeId`.** Both only read the
|
|
12
|
+
older `revision` option (`metaedit` also `change`), so
|
|
13
|
+
`describe({ changeId, message })` silently rewrote the working-copy change
|
|
14
|
+
instead of the named one -- the documented option (API.md) did nothing and
|
|
15
|
+
`DescribeArgs` in `types.d.ts` didn't list it. Both now accept `changeId`
|
|
16
|
+
(alongside `revision`), rewrite exactly that change, leave the working
|
|
17
|
+
copy's description alone, and keep change IDs stable. `DescribeArgs` gains
|
|
18
|
+
`changeId` and `revision`. Regression tests:
|
|
19
|
+
`tests/integration/describe-change-id.test.js`.
|
|
20
|
+
- **`operations.restore()` left changes created after the target operation
|
|
21
|
+
visible.** The restore operation recorded the right view (heads, working
|
|
22
|
+
copy), but the revset layer only hid orphaned changes that were empty and
|
|
23
|
+
undescribed, so a described change stayed in `log()`, `heads(all())` and
|
|
24
|
+
`visible_heads()`. As in jj, restoring now restores the *view*: every
|
|
25
|
+
change created after the target operation and unreachable from the
|
|
26
|
+
restored working copy, bookmarks and tags is hidden (not deleted -- it
|
|
27
|
+
stays in the graph and resolvable by change ID). The hidden set is stored
|
|
28
|
+
in the operation's `view.hiddenChanges`, so it persists across later
|
|
29
|
+
operations, `undo()`/`redo()` of the restore bring the changes back, and
|
|
30
|
+
they reappear if the working copy/bookmarks make them reachable again
|
|
31
|
+
(e.g. `edit({ changeId })`). **Behavior change:** `log()` after a restore
|
|
32
|
+
no longer lists such changes. Regression tests:
|
|
33
|
+
`tests/integration/op-restore-view.test.js`.
|
|
34
|
+
|
|
3
35
|
## 1.13.0 — 2026-10-05 — Write-path fixes: converge on a fresh instance, new files lost on edit(), rebase content
|
|
4
36
|
|
|
5
37
|
Fixed in [PR #52](https://github.com/johnhenry/isomorphic-jj/pull/52).
|
package/README.md
CHANGED
|
@@ -1020,7 +1020,7 @@ repo/
|
|
|
1020
1020
|
|
|
1021
1021
|
## Project Status
|
|
1022
1022
|
|
|
1023
|
-
**Current Version**: 1.
|
|
1023
|
+
**Current Version**: 1.14.0 (`@johnhenry/isomorphic-jj`; same library/API lineage as the old `isomorphic-jj`, which reached v1.8.0 before the rename — see the provenance note at the top of this file, and the CHANGELOG's `1.9.0` entry for how the scoped/unscoped version lineages were merged back together)
|
|
1024
1024
|
**Test Coverage**: 1882 tests passing, ~97% statements / 90% branches
|
|
1025
1025
|
**JJ CLI parity**: tracks Jujutsu through v0.45.1
|
|
1026
1026
|
**Status**: Actively developed; broad JJ CLI semantic parity, used in the examples/apps in this repo and in [JJHub](#family). Not yet 1.0 in the API-stability sense (the version number is past 1.0 only because merging the scoped/unscoped lineages forced it above the unscoped side's prior ceiling, not because the API is frozen) — API surface can still shift; pin a version and read the CHANGELOG before upgrading.
|
|
@@ -1038,6 +1038,7 @@ repo/
|
|
|
1038
1038
|
- ✅ 0.3.0: Parity refresh tracking jj through **v0.45.1** — a working `/browser` entry (async `createBrowserFS()`, bundle-safe protobuf schema loading, no stray Node-only imports), `undo()`/`operations.restore()` reverting graph rewrites with real progressive undo/redo, `squash()`/`abandon()`/`edit()`/`new()` fully syncing content instead of only metadata, `rebase()` performing a real three-way merge and recording conflicts, and the new `converge()` command (jj v0.45.0) to auto-resolve divergent commits. See [CHANGELOG.md](./CHANGELOG.md).
|
|
1039
1039
|
- ✅ 0.4.0: `converge()` generalized from pairwise to N-way — any number of divergent copies of one change id resolve in a single call, matching how jj's own underlying engine already treats divergence internally (issue #32). See [CHANGELOG.md](./CHANGELOG.md).
|
|
1040
1040
|
- ✅ 1.9.0: Scoped (`@johnhenry/isomorphic-jj`) and unscoped (`isomorphic-jj`) version lineages merged into one shared version going forward, with both names dual-published on every release.
|
|
1041
|
+
- ✅ 1.14.0: `describe()`/`metaedit()` honor `changeId`; `operations.restore()` restores the view (changes created after the target op are hidden from `log()`). See [CHANGELOG.md](./CHANGELOG.md).
|
|
1041
1042
|
- ✅ 1.13.0: Write-path fixes — `converge()` works on a fresh instance, `edit()` no longer deletes new files `write()` created in the change it leaves, and `moveChange()`/`rebase()` derives the rebased change's content (new parent + the change's own edits) instead of keeping its tree verbatim. Not done: a rebase re-derives only the moved change's content, not its descendants', and conflicts are detected per whole file, not per line. See [CHANGELOG.md](./CHANGELOG.md).
|
|
1042
1043
|
- ✅ 1.9.1 – 1.12.0: Operation-log reliability pass — `undo()`/`operations.restore()` correctness across write/snapshot/squash/rebase/abandon/edit, `log()` visibility fixes, and recording gaps in descendant rebases and conflict-resolution ops (issues #37–#39, #41, #43, #45, #48). See [CHANGELOG.md](./CHANGELOG.md) for the full per-fix breakdown.
|
|
1043
1044
|
|
package/dist/index.cjs
CHANGED
|
@@ -1517,6 +1517,24 @@ class OperationLog {
|
|
|
1517
1517
|
operation.parents = operation.parents || [];
|
|
1518
1518
|
}
|
|
1519
1519
|
|
|
1520
|
+
// `view.hiddenChanges` (written by operations.restore()) is part of the
|
|
1521
|
+
// view, so it persists across later operations. undo()/redo()/restore()
|
|
1522
|
+
// set it explicitly (their view is a snapshot of another operation);
|
|
1523
|
+
// every other operation carries the current head's value forward.
|
|
1524
|
+
if (
|
|
1525
|
+
this.headOperationId &&
|
|
1526
|
+
operation.view &&
|
|
1527
|
+
!('hiddenChanges' in operation.view) &&
|
|
1528
|
+
operation.eventType !== 'undo' &&
|
|
1529
|
+
operation.eventType !== 'redo'
|
|
1530
|
+
) {
|
|
1531
|
+
const headOp = this.operations.find((op) => op.id === this.headOperationId);
|
|
1532
|
+
const inherited = headOp.view && headOp.view.hiddenChanges;
|
|
1533
|
+
if (inherited && inherited.length > 0) {
|
|
1534
|
+
operation.view = { ...operation.view, hiddenChanges: inherited };
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1520
1538
|
// Generate operation ID
|
|
1521
1539
|
const id = await generateOperationId(operation);
|
|
1522
1540
|
const fullOperation = {
|
|
@@ -2763,16 +2781,18 @@ class RevsetEngine {
|
|
|
2763
2781
|
case 'visible_heads': {
|
|
2764
2782
|
await this.graph.load();
|
|
2765
2783
|
const allChanges = this.graph.getAll();
|
|
2766
|
-
const
|
|
2784
|
+
const restoreHidden = await this._computeRestoreHidden();
|
|
2785
|
+
const visibleChanges = allChanges.filter((c) => !restoreHidden.has(c.changeId));
|
|
2786
|
+
const changeIdSet = new Set(visibleChanges.map((c) => c.changeId));
|
|
2767
2787
|
const hasChildren = new Set();
|
|
2768
|
-
for (const change of
|
|
2788
|
+
for (const change of visibleChanges) {
|
|
2769
2789
|
if (change.parents) {
|
|
2770
2790
|
for (const parent of change.parents) {
|
|
2771
2791
|
if (changeIdSet.has(parent)) hasChildren.add(parent);
|
|
2772
2792
|
}
|
|
2773
2793
|
}
|
|
2774
2794
|
}
|
|
2775
|
-
return
|
|
2795
|
+
return visibleChanges.filter((c) => !hasChildren.has(c.changeId)).map((c) => c.changeId);
|
|
2776
2796
|
}
|
|
2777
2797
|
|
|
2778
2798
|
// `git_refs()` and `git_head()` are deprecated compatibility shims.
|
|
@@ -3258,6 +3278,49 @@ class RevsetEngine {
|
|
|
3258
3278
|
* @returns {Promise<Set<string>>} change IDs to exclude
|
|
3259
3279
|
*/
|
|
3260
3280
|
async _computeHiddenOrphans(allChanges) {
|
|
3281
|
+
const restoreHidden = await this._computeRestoreHidden();
|
|
3282
|
+
const hidden = await this._computeUnreferencedOrphans(allChanges);
|
|
3283
|
+
for (const id of restoreHidden) hidden.add(id);
|
|
3284
|
+
return hidden;
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
/**
|
|
3288
|
+
* Changes hidden by `operations.restore()` (issue #56): the head
|
|
3289
|
+
* operation's `view.hiddenChanges`, minus any that the current working
|
|
3290
|
+
* copy / bookmarks / tags make reachable again. Deliberately does NOT use
|
|
3291
|
+
* the operation log's recorded heads for reachability -- later operations
|
|
3292
|
+
* record structural heads, which would include the hidden leaf itself.
|
|
3293
|
+
*
|
|
3294
|
+
* @returns {Promise<Set<string>>}
|
|
3295
|
+
*/
|
|
3296
|
+
async _computeRestoreHidden() {
|
|
3297
|
+
const hidden = new Set();
|
|
3298
|
+
if (!this.oplog) return hidden;
|
|
3299
|
+
const headOp = await this.oplog.getHeadOperation();
|
|
3300
|
+
const listed = (headOp && headOp.view && headOp.view.hiddenChanges) || [];
|
|
3301
|
+
if (listed.length === 0) return hidden;
|
|
3302
|
+
|
|
3303
|
+
const roots = new Set([this.workingCopy.getCurrentChangeId()]);
|
|
3304
|
+
await this.bookmarkStore.load();
|
|
3305
|
+
for (const bookmark of await this.bookmarkStore.list()) roots.add(bookmark.changeId);
|
|
3306
|
+
for (const tag of await this.tagStore.list()) roots.add(tag.changeId);
|
|
3307
|
+
const reachable = new Set();
|
|
3308
|
+
for (const rootId of roots) {
|
|
3309
|
+
for (const id of await this.getAncestors(rootId)) reachable.add(id);
|
|
3310
|
+
}
|
|
3311
|
+
for (const id of listed) {
|
|
3312
|
+
if (!reachable.has(id)) hidden.add(id);
|
|
3313
|
+
}
|
|
3314
|
+
return hidden;
|
|
3315
|
+
}
|
|
3316
|
+
|
|
3317
|
+
/**
|
|
3318
|
+
* The #37(b)/#43(2)/#48 rule: unreachable, undescribed orphans.
|
|
3319
|
+
*
|
|
3320
|
+
* @param {any[]} allChanges
|
|
3321
|
+
* @returns {Promise<Set<string>>}
|
|
3322
|
+
*/
|
|
3323
|
+
async _computeUnreferencedOrphans(allChanges) {
|
|
3261
3324
|
const trackedHeads = new Set();
|
|
3262
3325
|
try {
|
|
3263
3326
|
const wcId = this.workingCopy && this.workingCopy.getCurrentChangeId();
|
|
@@ -8203,6 +8266,61 @@ async function createJJ(options) {
|
|
|
8203
8266
|
return heads;
|
|
8204
8267
|
};
|
|
8205
8268
|
|
|
8269
|
+
/**
|
|
8270
|
+
* Changes that `operations.restore(targetOp)` must hide (issue #56): every
|
|
8271
|
+
* change created after `targetOp` (i.e. not among the heads / working copy /
|
|
8272
|
+
* touched changes recorded up to targetOp, nor their ancestors) that is not
|
|
8273
|
+
* reachable from the restored working copy, bookmarks or tags. Hidden, not
|
|
8274
|
+
* deleted -- they reappear if a later operation makes them reachable again.
|
|
8275
|
+
* Previously hidden changes that targetOp's own view already hid stay hidden.
|
|
8276
|
+
*
|
|
8277
|
+
* @param {any[]} ops - Full operation list (chronological)
|
|
8278
|
+
* @param {any} targetOp
|
|
8279
|
+
* @returns {Promise<string[]>}
|
|
8280
|
+
*/
|
|
8281
|
+
const computeRestoreHiddenChanges = async (ops, targetOp) => {
|
|
8282
|
+
await graph.load();
|
|
8283
|
+
const ancestorsOf = (/** @type {Iterable<string>} */ seeds) => {
|
|
8284
|
+
const seen = new Set();
|
|
8285
|
+
const stack = [...seeds];
|
|
8286
|
+
while (stack.length > 0) {
|
|
8287
|
+
const id = /** @type {string} */ (stack.pop());
|
|
8288
|
+
if (seen.has(id)) continue;
|
|
8289
|
+
seen.add(id);
|
|
8290
|
+
for (const parent of graph.getParents(id)) stack.push(parent);
|
|
8291
|
+
}
|
|
8292
|
+
return seen;
|
|
8293
|
+
};
|
|
8294
|
+
|
|
8295
|
+
const targetIndex = ops.findIndex((o) => o.id === targetOp.id);
|
|
8296
|
+
const knownSeeds = new Set();
|
|
8297
|
+
for (const op of ops.slice(0, targetIndex + 1)) {
|
|
8298
|
+
const view = op.view || {};
|
|
8299
|
+
for (const id of view.heads || []) knownSeeds.add(id);
|
|
8300
|
+
if (view.workingCopy) knownSeeds.add(view.workingCopy);
|
|
8301
|
+
for (const id of Object.keys(op.changeSnapshot || {})) knownSeeds.add(id);
|
|
8302
|
+
}
|
|
8303
|
+
const known = ancestorsOf(knownSeeds);
|
|
8304
|
+
|
|
8305
|
+
const roots = new Set([workingCopy.getCurrentChangeId()]);
|
|
8306
|
+
await bookmarks.load();
|
|
8307
|
+
for (const bookmark of await bookmarks.list()) roots.add(bookmark.changeId);
|
|
8308
|
+
for (const tag of await tags.list()) roots.add(tag.changeId);
|
|
8309
|
+
const reachable = ancestorsOf(roots);
|
|
8310
|
+
|
|
8311
|
+
const hidden = new Set(
|
|
8312
|
+
((targetOp.view && targetOp.view.hiddenChanges) || []).filter(
|
|
8313
|
+
(/** @type {string} */ id) => !reachable.has(id)
|
|
8314
|
+
)
|
|
8315
|
+
);
|
|
8316
|
+
for (const change of graph.getAll()) {
|
|
8317
|
+
if (!known.has(change.changeId) && !reachable.has(change.changeId)) {
|
|
8318
|
+
hidden.add(change.changeId);
|
|
8319
|
+
}
|
|
8320
|
+
}
|
|
8321
|
+
return [...hidden];
|
|
8322
|
+
};
|
|
8323
|
+
|
|
8206
8324
|
/**
|
|
8207
8325
|
* Re-parent every child of `changeId` onto `newParents` instead — used
|
|
8208
8326
|
* when a change is being elided from the graph (abandon(), issue #30;
|
|
@@ -9537,6 +9655,11 @@ async function createJJ(options) {
|
|
|
9537
9655
|
* @param {Record<string, any>} [args]
|
|
9538
9656
|
*/
|
|
9539
9657
|
async describe(args = {}) {
|
|
9658
|
+
// Documented option is `changeId`; `revision` is the older spelling.
|
|
9659
|
+
if (args.changeId && !args.revision) {
|
|
9660
|
+
args = { ...args, revision: args.changeId };
|
|
9661
|
+
}
|
|
9662
|
+
|
|
9540
9663
|
await graph.load();
|
|
9541
9664
|
await workingCopy.load();
|
|
9542
9665
|
await userConfig.load();
|
|
@@ -9686,10 +9809,13 @@ async function createJJ(options) {
|
|
|
9686
9809
|
* @returns {Promise<Object>} Updated change object
|
|
9687
9810
|
*/
|
|
9688
9811
|
async metaedit(args = {}) {
|
|
9689
|
-
// Normalize: accept 'change' as
|
|
9812
|
+
// Normalize: accept 'change' / 'changeId' as aliases for 'revision'
|
|
9690
9813
|
if (args.change && !args.revision) {
|
|
9691
9814
|
args = { ...args, revision: args.change };
|
|
9692
9815
|
}
|
|
9816
|
+
if (args.changeId && !args.revision) {
|
|
9817
|
+
args = { ...args, revision: args.changeId };
|
|
9818
|
+
}
|
|
9693
9819
|
|
|
9694
9820
|
// Normalize: accept 'message' as alias for 'description'
|
|
9695
9821
|
if (args.message && !args.description) {
|
|
@@ -10617,7 +10743,11 @@ async function createJJ(options) {
|
|
|
10617
10743
|
redoFileSnapshot,
|
|
10618
10744
|
redoConflictsSnapshot,
|
|
10619
10745
|
parents: [],
|
|
10620
|
-
view: {
|
|
10746
|
+
view: {
|
|
10747
|
+
...landingView,
|
|
10748
|
+
fileSnapshot: diskSnapshotBeforeThisOp,
|
|
10749
|
+
hiddenChanges: landingView.hiddenChanges || [],
|
|
10750
|
+
},
|
|
10621
10751
|
});
|
|
10622
10752
|
|
|
10623
10753
|
const restoredFileCount =
|
|
@@ -10740,7 +10870,11 @@ async function createJJ(options) {
|
|
|
10740
10870
|
redoOf: undoOp.id,
|
|
10741
10871
|
changeSnapshot: undoChangeSnapshot,
|
|
10742
10872
|
parents: [],
|
|
10743
|
-
view: {
|
|
10873
|
+
view: {
|
|
10874
|
+
...redoneView,
|
|
10875
|
+
fileSnapshot: diskSnapshotBeforeThisOp,
|
|
10876
|
+
hiddenChanges: redoneView.hiddenChanges || [],
|
|
10877
|
+
},
|
|
10744
10878
|
});
|
|
10745
10879
|
|
|
10746
10880
|
const restoredFileCount =
|
|
@@ -11072,6 +11206,13 @@ async function createJJ(options) {
|
|
|
11072
11206
|
|
|
11073
11207
|
await applyGraphReversal(reversal);
|
|
11074
11208
|
|
|
11209
|
+
// Restoring the view hides (not deletes) every change that was
|
|
11210
|
+
// created after targetOp and is not reachable from the restored
|
|
11211
|
+
// working copy / bookmarks / tags -- see issue #56. The list is
|
|
11212
|
+
// recorded in the view so it survives later operations, and so
|
|
11213
|
+
// undo()/redo() of the restore bring the changes back.
|
|
11214
|
+
const hiddenChanges = await computeRestoreHiddenChanges(ops, targetOp);
|
|
11215
|
+
|
|
11075
11216
|
// Record this restoration as a new operation
|
|
11076
11217
|
await oplog.recordOperation({
|
|
11077
11218
|
timestamp: new Date().toISOString(),
|
|
@@ -11079,7 +11220,7 @@ async function createJJ(options) {
|
|
|
11079
11220
|
description: `restore to operation ${args.operation}`,
|
|
11080
11221
|
parents: [],
|
|
11081
11222
|
changeSnapshot,
|
|
11082
|
-
view: { ...targetOp.view, fileSnapshot: diskSnapshotBeforeThisOp },
|
|
11223
|
+
view: { ...targetOp.view, fileSnapshot: diskSnapshotBeforeThisOp, hiddenChanges },
|
|
11083
11224
|
});
|
|
11084
11225
|
|
|
11085
11226
|
return {
|